第1章 安裝
1.1 基礎配置
[root@k8s_master ~]# cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core)
[root@k8s_master ~]# systemctl status firewalld
● firewalld.service - firewalld - dynamic firewall daemon
Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled)
Active: inactive (dead)
Docs: man:firewalld(1)
[root@k8s_master ~]# setenforce 0
setenforce: SELinux is disabled
[root@k8s_master ~]# cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.138.150 k8s_master
192.168.138.151 k8s_client1
192.168.138.152 k8s_client2
配置阿里雲的yum源或系統默認的源;
[root@k8s_master ~]# yum -y install ntp docker
[root@k8s_master ~]# systemctl start ntpd
[root@k8s_master ~]# systemctl enable ntpd
Created symlink from /etc/systemd/system/multi-user.target.wants/ntpd.service to /usr/lib/systemd/system/ntpd.service.
Master節點操作
安裝etcd
[root@k8s_master ~]# yum -y install etcd
[root@k8s_master ~]# cat /etc/etcd/etcd.conf | grep -v "^#"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379,http://0.0.0.0:4001"
ETCD_NAME="master"
ETCD_ADVERTISE_CLIENT_URLS=http://k8s_master:2379,http://k8s_master:4001
[root@k8s_master ~]# systemctl enable etcd
[root@k8s_master ~]# systemctl start etcd
[root@k8s_master ~]# etcdctl -C http://k8s_master:2379 cluster-health
member 8e9e05c52164694d is healthy: got healthy result from http://k8s_master:2379
cluster is healthy
[root@k8s_master ~]# etcdctl -C http://k8s_master:4001 cluster-health
member 8e9e05c52164694d is healthy: got healthy result from http://k8s_master:2379
cluster is healthy
安裝docker(所有節點)
[root@k8s_master ~]# yum -y install docker
[root@k8s_master ~]# docker version
Client:
Version: 1.13.1
API version: 1.26
Package version: docker-1.13.1-74.git6e3bb8e.el7.centos.x86_64
Go version: go1.9.4
Git commit: 6e3bb8e/1.13.1
Built: Tue Aug 21 15:23:37 2018
OS/Arch: linux/amd64
Server:
Version: 1.13.1
API version: 1.26 (minimum version 1.12)
Package version: docker-1.13.1-74.git6e3bb8e.el7.centos.x86_64
Go version: go1.9.4
Git commit: 6e3bb8e/1.13.1
Built: Tue Aug 21 15:23:37 2018
OS/Arch: linux/amd64
Experimental: false
[root@k8s_master ~]# yum install kubernetes
[root@k8s_master ~]# cat /etc/kubernetes/apiserver | grep -Ev "^#|^$"
KUBE_API_ADDRESS="--insecure-bind-address=0.0.0.0"
KUBE_API_PORT="--port=8080"
KUBE_ETCD_SERVERS="--etcd-servers=http://192.168.138.150:2379"
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"
KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,ResourceQuota"
KUBE_API_ARGS=""
[root@k8s_master ~]# cat /etc/kubernetes/config |grep -Ev "^#|^$"
KUBE_LOGTOSTDERR="--logtostderr=true"
KUBE_LOG_LEVEL="--v=0"
KUBE_ALLOW_PRIV="--allow-privileged=false"
KUBE_MASTER="--master=http://192.168.138.150:8080"
[root@k8s_master ~]# systemctl enable kube-apiserver kube-controller-manager kube-scheduler
[root@k8s_master ~]# systemctl start kube-apiserver kube-controller-manager kube-scheduler
[root@k8s_master ~]# netstat -lntup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:2380 0.0.0.0:* LISTEN 936/etcd
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 639/rpcbind
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 932/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1026/master
tcp6 0 0 :::4001 :::* LISTEN 936/etcd
tcp6 0 0 :::6443 :::* LISTEN 1047/kube-apiserver
tcp6 0 0 :::2379 :::* LISTEN 936/etcd
tcp6 0 0 :::10251 :::* LISTEN 651/kube-scheduler
tcp6 0 0 :::10252 :::* LISTEN 634/kube-controller
tcp6 0 0 :::111 :::* LISTEN 639/rpcbind
tcp6 0 0 :::8080 :::* LISTEN 1047/kube-apiserver
tcp6 0 0 ::1:25 :::* LISTEN 1026/master
udp 0 0 0.0.0.0:803 0.0.0.0:* 639/rpcbind
udp 0 0 0.0.0.0:68 0.0.0.0:* 2964/dhclient
udp 0 0 192.168.138.150:8285 0.0.0.0:* 1048/flanneld
udp 0 0 0.0.0.0:111 0.0.0.0:* 639/rpcbind
udp 0 0 192.168.138.150:123 0.0.0.0:* 2555/ntpd
node節點安裝
[root@k8s_client1 ~]# yum install kubernetes
[root@k8s_client1 ~]# cat /etc/kubernetes/config | egrep -v "^#|^$"
KUBE_LOGTOSTDERR="--logtostderr=true"
KUBE_LOG_LEVEL="--v=0"
KUBE_ALLOW_PRIV="--allow-privileged=false"
KUBE_MASTER="--master=http://192.168.138.150:8080"
[root@k8s_client1 ~]# cat /etc/kubernetes/kubelet | egrep -v "^#|^$"
KUBELET_ADDRESS="--address=0.0.0.0"
KUBELET_PORT="--port=10250"
KUBELET_HOSTNAME="--hostname-override=192.168.138.151"
KUBELET_API_SERVER="--api-servers=http://192.168.138.150:8080"
KUBELET_POD_INFRA_CONTAINER="--pod-infra-container-image=registry.access.redhat.com/rhel7/pod-infrastructure:latest"
KUBELET_ARGS=""
[root@k8s_client1 ~]# systemctl enable kubelet kube-proxy
[root@k8s_client1 ~]# systemctl start kubelet kube-proxy
[root@k8s_client1 ~]# netstat -lntup
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:10248 0.0.0.0:* LISTEN 1376/kubelet
tcp 0 0 127.0.0.1:10249 0.0.0.0:* LISTEN 925/kube-proxy
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 635/rpcbind
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 927/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1014/master
tcp6 0 0 :::4194 :::* LISTEN 1376/kubelet
tcp6 0 0 :::30372 :::* LISTEN 925/kube-proxy
tcp6 0 0 :::10250 :::* LISTEN 1376/kubelet
tcp6 0 0 :::9100 :::* LISTEN 1947/node_exporter
tcp6 0 0 :::10255 :::* LISTEN 1376/kubelet
tcp6 0 0 :::111 :::* LISTEN 635/rpcbind
tcp6 0 0 :::30000 :::* LISTEN 925/kube-proxy
tcp6 0 0 :::22 :::* LISTEN 927/sshd
tcp6 0 0 ::1:25 :::* LISTEN 1014/master
udp 0 0 0.0.0.0:807 0.0.0.0:* 635/rpcbind
udp 0 0 0.0.0.0:68 0.0.0.0:* 744/dhclient
udp 0 0 192.168.138.151:8285 0.0.0.0:* 930/flanneld
[root@k8s_master ~]# kubectl get nodes
NAME STATUS AGE
192.168.138.151 Ready 15d
192.168.138.152 Ready 15d
安裝flannel(所有節點)
yum install flannel
systemctl status firewalld
systemctl stop firewalld
systemctl disable firewalld
Master節點
[root@k8s_master ~]# cat /etc/sysconfig/flanneld | egrep -v "^#|^$"
FLANNEL_ETCD_ENDPOINTS="http://192.168.138.150:2379"
FLANNEL_ETCD_PREFIX="/atomic.io/network"
[root@k8s_master ~]# etcdctl mk //atomic.io/network/config '{"Network":"172.8.0.0/16"}'
[root@k8s_master ~]# systemctl enable flanneld
[root@k8s_master ~]# systemctl start flanneld
[root@k8s_master ~]# for SERVICES in docker kube-apiserver kube-controller-manager kube-scheduler; do systemctl restart $SERVICES ; done
Node節點
[root@k8s_client1 ~]# cat /etc/sysconfig/flanneld | egrep -v "^#|^$"
FLANNEL_ETCD_ENDPOINTS="http://192.168.138.150:2379"
FLANNEL_ETCD_PREFIX="/atomic.io/network"
[root@k8s_client1 ~]# etcdctl mk //atomic.io/network/config '{"Network":"172.8.0.0/16"}'
[root@k8s_client1 ~]# systemctl enable flanneld
[root@k8s_client1 ~]# systemctl start flanneld
[root@k8s_client1 ~]# systemctl restart kube-proxy kubelet docker
[root@k8s_client1 ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eno16777728: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:f2:c5:36 brd ff:ff:ff:ff:ff:ff
inet 192.168.138.151/24 brd 192.168.138.255 scope global noprefixroute dynamic eno16777728
valid_lft 1126sec preferred_lft 1126sec
inet6 fe80::20c:29ff:fef2:c536/64 scope link
valid_lft forever preferred_lft forever
3: flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500
link/none
inet 172.8.77.0/16 scope global flannel0
valid_lft forever preferred_lft forever
inet6 fe80::4ac1:c034:5096:3b04/64 scope link flags 800
valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue state UP group default
link/ether 02:42:27:e3:50:d7 brd ff:ff:ff:ff:ff:ff
inet 172.8.77.1/24 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:27ff:fee3:50d7/64 scope link
valid_lft forever preferred_lft forever
所有節點
yum install *rhsm* -y
docker pull registry.access.redhat.com/rhel7/pod-infrastructure:latest (k8s運行docker的基礎鏡像)
在阿里雲上拿下來的
部署dashboard
[root@k8s_master ~]# docker pull docker.io/siriuszg/kubernetes-dashboard-amd64:v1.5.1
[root@k8s_master ~]# cat kubernetes-dashboard.yaml
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
labels:
app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kube-system
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: kubernetes-dashboard
template:
metadata:
labels:
app: kubernetes-dashboard
# Comment the following annotation if Dashboard must not be deployed on master
annotations:
scheduler.alpha.kubernetes.io/tolerations: |
[
{
"key": "dedicated",
"operator": "Equal",
"value": "master",
"effect": "NoSchedule"
}
]
spec:
containers:
- name: kubernetes-dashboard
image: docker.io/siriuszg/kubernetes-dashboard-amd64:v1.5.1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9090
protocol: TCP
args:
# Uncomment the following line to manually specify Kubernetes API server Host
# If not specified, Dashboard will attempt to auto discover the API server and connect
# to it. Uncomment only if the default does not work.
- --apiserver-host=http://192.168.138.150:8080
livenessProbe:
httpGet:
path: /
port: 9090
initialDelaySeconds: 30
timeoutSeconds: 30
[root@k8s_master ~]# cat dashboard-service.yaml
kind: Service
apiVersion: v1
metadata:
labels:
app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kube-system
spec:
type: NodePort
ports:
- port: 80
targetPort: 9090
selector:
app: kubernetes-dashboard
[root@k8s_master ~]# kubectl create -f kubernetes-dashboard.yaml
deployment "kubernetes-dashboard" created
[root@k8s_master ~]# kubectl create -f dashboard-service.yaml
service "kubernetes-dashboard" created
[root@k8s_master ~]# kubectl get deployment --all-namespaces
NAMESPACE NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
default httpd 3 3 3 3 14d
default nginx-deployment 3 3 3 3 14d
kube-system kubernetes-dashboard 1 1 1 1 15d
kube-system tiller-deploy 1 1 1 0 13d
weave weave-scope-app 1 1 1 1 13d
[root@k8s_master ~]# kubectl get svc --all-namespaces
NAMESPACE NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default httpd-svc 10.254.146.62 <nodes> 8082:30000/TCP 14d
default kubernetes 10.254.0.1 <none> 443/TCP 15d
kube-system kubernetes-dashboard 10.254.172.95 <nodes> 80:30372/TCP 15d
kube-system tiller-deploy 10.254.112.169 <none> 44134/TCP 13d
weave weave-scope-app 10.254.187.130 <none> 80/TCP 13d
訪問:192.168.138.150:8080/ui
參考文檔:http://blog.51cto.com/jonauil/2084986
https://www.cnblogs.com/clsn/p/8410321.html
第2章 概念
Kubernetes 具備完善的集群管理能力,包括多層次的安全防護和准入機制、多租戶應用支撐能力、透明的服務注冊和服務發現機制、內建負載均衡器、故障發現和自我修復能力、服務滾動升級和在線擴容、可擴展的資源自動 調度機制、多粒度的資源配額管理能力。
Kubernetes 還提供完善的管理工具,涵蓋開發、部署測試、運維監控等各個環節。
Kubernetes主要由以下幾個核心組件組成:
etcd保存了整個集群的狀態;
apiserver提供了資源操作的唯一入口,並提供認證、授權、訪問控制、API注冊和發現等機制;
controller manager負責維護集群的狀態,比如故障檢測、自動擴展、滾動更新等;
scheduler負責資源的調度,按照預定的調度策略將Pod調度到相應的機器上;
kubelet負責維護容器的生命周期,同時也負責Volume(CVI)和網絡(CNI)的管理;
Container runtime負責鏡像管理以及Pod和容器的真正運行(CRI);
kube-proxy負責為Service提供cluster內部的服務發現和負載均衡;
kube-dns負責為整個集群提供DNS服務
Ingress Controller為服務提供外網入口
Heapster提供資源監控
Dashboard提供GUI
Federation提供跨可用區的集群
Fluentd-elasticsearch提供集群日志采集、存儲與查詢
Kubernetes設計理念和功能其實就是一個類似Linux的分層架構,如下圖所示:
Cluster
Cluster 是計算、存儲和網絡資源的集合,Kubernetes 利用這些資源運行各種基於容器的應用。
Master
Master 是 Cluster 的大腦,它的主要職責是調度,即決定將應用放在哪里運行。Master 運行 Linux 操作系統,可以是物理機或者虛擬機。為了實現高可用,可以運行多個 Master。
Node
Node 的職責是運行容器應用。Node 由 Master 管理,Node 負責監控並匯報容器的狀態,並根據 Master 的要求管理容器的生命周期。Node 運行在 Linux 操作系統,可以是物理機或者是虛擬機。
Pod
Pod 是 Kubernetes 的最小工作單元。每個 Pod 包含一個或多個容器。Pod 中的容器會作為一個整體被 Master 調度到一個 Node 上運行。
Kubernetes 引入 Pod 主要基於下面兩個目的:
可管理性。
有些容器天生就是需要緊密聯系,一起工作。Pod 提供了比容器更高層次的抽象,將它們封裝到一個部署單元中。Kubernetes 以 Pod 為最小單位進行調度、擴展、共享資源、管理生命周期。
通信和資源共享。
Pod 中的所有容器使用同一個網絡 namespace,即相同的 IP 地址和 Port 空間。它們可以直接用 localhost 通信。同樣的,這些容器可以共享存儲,當 Kubernetes 掛載 volume 到 Pod,本質上是將 volume 掛載到 Pod 中的每一個容器。
Pods 有兩種使用方式:
運行單一容器。
one-container-per-Pod 是 Kubernetes 最常見的模型,這種情況下,只是將單個容器簡單封裝成 Pod。即便是只有一個容器,Kubernetes 管理的也是 Pod 而不是直接管理容器。
運行多個容器。
但問題在於:哪些容器應該放到一個 Pod 中?
答案是:這些容器聯系必須 非常緊密,而且需要 直接共享資源。
Controller
Kubernetes 通常不會直接創建 Pod,而是通過 Controller 來管理 Pod 的。Controller 中定義了 Pod 的部署特性,比如有幾個副本,在什么樣的 Node 上運行等。為了滿足不同的業務場景,Kubernetes 提供了多種 Controller,包括 Deployment、ReplicaSet、DaemonSet、StatefuleSet、Job 等,我們逐一討論。
Deployment 是最常用的 Controller,比如前面在線教程中就是通過創建 Deployment 來部署應用的。Deployment 可以管理 Pod 的多個副本,並確保 Pod 按照期望的狀態運行。
ReplicaSet 實現了 Pod 的多副本管理。使用 Deployment 時會自動創建 ReplicaSet,也就是說 Deployment 是通過 ReplicaSet 來管理 Pod 的多個副本,我們通常不需要直接使用 ReplicaSet。
DaemonSet 用於每個 Node 最多只運行一個 Pod 副本的場景。正如其名稱所揭示的,DaemonSet 通常用於運行 daemon。
StatefuleSet 能夠保證 Pod 的每個副本在整個生命周期中名稱是不變的。而其他 Controller 不提供這個功能,當某個 Pod 發生故障需要刪除並重新啟動時,Pod 的名稱會發生變化。同時 StatefuleSet 會保證副本按照固定的順序啟動、更新或者刪除。
Job 用於運行結束就刪除的應用。而其他 Controller 中的 Pod 通常是長期持續運行。
Service
Deployment 可以部署多個副本,每個 Pod 都有自己的 IP,外界如何訪問這些副本呢?
通過 Pod 的 IP 嗎?
要知道 Pod 很可能會被頻繁地銷毀和重啟,它們的 IP 會發生變化,用 IP 來訪問不太現實。
答案是 Service。
Kubernetes Service 定義了外界訪問一組特定 Pod 的方式。Service 有自己的 IP 和端口,Service 為 Pod 提供了負載均衡。
Namespace
如果有多個用戶或項目組使用同一個 Kubernetes Cluster,如何將他們創建的 Controller、Pod 等資源分開呢?
答案就是 Namespace。
Namespace 可以將一個物理的 Cluster 邏輯上划分成多個虛擬 Cluster,每個 Cluster 就是一個 Namespace。不同 Namespace 里的資源是完全隔離的。
Kubernetes 默認創建了兩個 Namespace。
default -- 創建資源時如果不指定,將被放到這個 Namespace 中。
kube-system -- Kubernetes 自己創建的系統資源將放到這個 Namespace 中
第3章 k8s架構
Master是Kubernetes Cluster的大腦,運行着如下Daemon服務:kube-apiserver、kube-scheduler、kube-controller-manager、etcd和Pod網絡(例如 flannel)。
API Server(kube-apiserver)
API Server提供HTTP/HTTPS RESTful API,即Kubernetes API。API Server是Kubernetes Cluster的前端接口,各種客戶端工具(CLI 或 UI)以及 Kubernetes其他組件可以通過它管理 Cluster 的各種資源。
Scheduler(kube-scheduler)
Scheduler負責決定將Pod 放在哪個 Node上運行。Scheduler在調度時會充分考慮Cluster 的拓撲結構,當前各個節點的負載,以及應用對高可用、性能、數據親和性的需求。
Controller Manager(kube-controller-manager)
Controller Manager 負責管理 Cluster 各種資源,保證資源處於預期的狀態。Controller Manager由多種 controller組成,包括 replication controller、endpoints controller、namespace controller、serviceaccounts controller 等。
不同的 controller 管理不同的資源。例如 replication controller 管理 Deployment、StatefulSet、DaemonSet 的生命周期,namespace controller 管理 Namespace 資源。
etcd
etcd 負責保存 Kubernetes Cluster 的配置信息和各種資源的狀態信息。當數據發生變化時,etcd 會快速地通知 Kubernetes 相關組件。
Pod 網絡
Pod 要能夠相互通信,Kubernetes Cluster 必須部署 Pod 網絡,flannel 是其中一個可選方案。
Node是Pod運行的地方,Kubernetes支持 Docker、rkt 等容器 Runtime。Node上運行的 Kubernetes 組件有kubelet、kube-proxy 和Pod網絡(例如 flannel)。
kubelet
kubelet 是 Node 的 agent,當 Scheduler 確定在某個 Node 上運行 Pod 后,會將 Pod 的具體配置信息(image、volume 等)發送給該節點的 kubelet,kubelet 根據這些信息創建和運行容器,並向 Master 報告運行狀態。
kube-proxy
service 在邏輯上代表了后端的多個 Pod,外界通過 service 訪問 Pod。service接收到的請求是如何轉發到 Pod 的呢?這就是 kube-proxy 要完成的工作。
每個 Node 都會運行 kube-proxy 服務,它負責將訪問 service 的 TCP/UPD 數據流轉發到后端的容器。如果有多個副本,kube-proxy 會實現負載均衡。
Pod 網絡
Pod 要能夠相互通信,Kubernetes Cluster必須部署Pod網絡,flannel是其中一個可選方案。
[root@k8s_master ~]# kubectl get pod --all-namespaces -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE
default redis 1/1 Running 3 1h 172.8.95.2 192.168.138.130
kube-system kubernetes-dashboard-2318246707-6vmhg 1/1 Running 4 1h 172.8.57.2 192.168.138.162
提前pull好鏡像;
[root@k8s_master ~]# kubectl run httpd-app --image=httpd --replicas=2
deployment "httpd-app" created
[root@k8s_master ~]# kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
httpd-app 2 2 2 2 16s
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpd-app-3361905436-4p8vk 1/1 Running 0 30s 172.8.18.2 192.168.138.161
httpd-app-3361905436-cq6xd 1/1 Running 0 30s 172.8.95.3 192.168.138.130
redis 1/1 Running 3 1h 172.8.95.2 192.168.138.130
kubectl 發送部署請求到 API Server。
API Server 通知 Controller Manager 創建一個 deployment 資源。
Scheduler 執行調度任務,將兩個副本 Pod 分發到 k8s-node1 和 k8s-node2。
k8s-node1 和 k8s-node2 上的 kubelet 在各自的節點上創建並運行 Pod。
補充兩點:
應用的配置和當前狀態信息保存在etcd 中,執行kubectl get pod 時,API Server 會從 etcd 中讀取這些數據。
flannel會為每個Pod都分配IP。因為沒有創建service,目前kube-proxy 還沒參與進來。
第4章 運行應用
前面我們已經了解到,Kubernetes通過各種Controller來管理Pod的生命周期。為了滿足不同業務場景,Kubernetes開發了Deployment、ReplicaSet、DaemonSet、StatefuleSet、Job 等多種Controller。
4.1 Deployment
[root@k8s_master ~]# kubectl get deployment httpd-app
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
httpd-app 2 2 2 2 24m
[root@k8s_master ~]# kubectl describe deployment httpd-app
Name: httpd-app
Namespace: default
CreationTimestamp: Thu, 30 Aug 2018 19:57:36 +0800
Labels: run=httpd-app
Selector: run=httpd-app
Replicas: 2 updated | 2 total | 2 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 1 max unavailable, 1 max surge
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
OldReplicaSets: <none>
NewReplicaSet: httpd-app-3361905436 (2/2 replicas created)
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
25m 25m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set httpd-app-3361905436 to 2
大部分內容都是自解釋的,我們重點看最下面部分。這里告訴我們創建了一個 ReplicaSet nginx-deployment-1260880958,Events 是 Deployment 的日志,記錄了ReplicaSet 的啟動過程。
通過上面的分析,也驗證了Deployment通過ReplicaSet來管理Pod的事實接着我們將注意力切換到 nginx-deployment-1260880958,執行 kubectl describe replicaset:
Controlled By指明此ReplicaSet是由 Deployment nginx-deployment創建。Events記錄了兩個副本Pod的創建。接着我們來看Pod執行 kubectl get pod:
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
httpd-app-3361905436-4p8vk 1/1 Running 0 2h
httpd-app-3361905436-cq6xd 1/1 Running 0 2h
redis 1/1 Running 3 3h
Controlled By 指明此Pod是由ReplicaSet nginx-deployment-1260880958 創建。Events記錄了Pod的啟動過程。如果操作失敗(比如 image 不存在)也能在這里查看到原因。
總結一下這個過程:
用戶通過 kubectl 創建 Deployment。
Deployment 創建 ReplicaSet。
ReplicaSet 創建 Pod。
從上圖也可以看出,對象的命名方式是:子對象的名字=父對象名字 + 隨機字符串或數字。
4.2 K8s資源創建方式
Kubernetes 支持兩種方式創建資源:
1.用 kubectl 命令直接創建,比如:
kubectl run nginx-deployment --image=nginx:1.7.9 --replicas=2
在命令行中通過參數指定資源的屬性。
2. 通過配置文件和 kubectl apply 創建,要完成前面同樣的工作,可執行命令:
kubectl apply -f nginx.yml
nginx.yml
資源的屬性寫在配置文件中,文件格式為 YAML。
下面對這兩種方式進行比較。
基於命令的方式:
簡單直觀快捷,上手快。
適合臨時測試或實驗。
基於配置文件的方式:
配置文件描述了 What,即應用最終要達到的狀態。
配置文件提供了創建資源的模板,能夠重復部署。
可以像管理代碼一樣管理部署。
適合正式的、跨環境的、規模化部署。
這種方式要求熟悉配置文件的語法,有一定難度。
后面我們都將采用配置文件的方式,大家需要盡快熟悉和掌握。
kubectl apply 不但能夠創建 Kubernetes 資源,也能對資源進行更新,非常方便。不過 Kubernets 還提供了幾個類似的命令,例如 kubectl create、kubectl replace、kubectl edit 和 kubectl patch。
為避免造成不必要的困擾,我們會盡量只使用 kubectl apply
[root@k8s_master ~]# kubectl apply -f nginx.yml
deployment "nginx-deployment" created
[root@k8s_master ~]# cat nginx.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 2
template:
metadata:
labels:
app: web_server
spec:
containers:
- name: nginx
image: nginx:1.7.9
[root@k8s_master ~]# kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
default httpd-app-3361905436-4p8vk 1/1 Running 0 2h
default httpd-app-3361905436-cq6xd 1/1 Running 0 2h
default nginx-deployment-2472058307-1lrwc 1/1 Running 0 56s
default nginx-deployment-2472058307-n786m 1/1 Running 0 56s
default redis 1/1 Running 3 4h
kube-system kubernetes-dashboard-2318246707-6vmhg 1/1 Running 4 4h
yaml配置文件解釋
apiVersion 是當前配置格式的版本。
kind 是要創建的資源類型,這里是 Deployment。
metadata 是該資源的元數據,name 是必需的元數據項。
spec 部分是該 Deployment 的規格說明。
replicas 指明副本數量,默認為 1。
template 定義 Pod 的模板,這是配置文件的重要部分。
metadata 定義 Pod 的元數據,至少要定義一個 label。label 的 key 和 value 可以任意指定。
spec 描述 Pod 的規格,此部分定義 Pod 中每一個容器的屬性,name 和 image 是必需的。
此nginx.yml是一個最簡單的Deployment配置文件,后面我們學習Kubernetes各項功能時會逐步豐富這個文件。
執行 kubectl apply -f nginx.yml:
[root@k8s_master ~]# kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
httpd-app 2 2 2 2 2h
nginx-deployment 2 2 2 2 7m
[root@k8s_master ~]# kubectl get replicaset
NAME DESIRED CURRENT READY AGE
httpd-app-3361905436 2 2 2 2h
nginx-deployment-2472058307 2 2 2 7m
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpd-app-3361905436-4p8vk 1/1 Running 0 2h 172.8.18.2 192.168.138.161
httpd-app-3361905436-cq6xd 1/1 Running 0 2h 172.8.95.3 192.168.138.130
nginx-deployment-2472058307-1lrwc 1/1 Running 0 7m 172.8.57.3 192.168.138.162
nginx-deployment-2472058307-n786m 1/1 Running 0 7m 172.8.18.3 192.168.138.161
redis 1/1 Running 3 4h 172.8.95.2 192.168.138.130
Deployment、ReplicaSet、Pod 都已經就緒。如果要刪除這些資源,執行kubectl delete deployment nginx-deployment或者kubectl delete -f nginx.yml。
4.3 Scale Up/Down
伸縮(Scale Up/Down)是指在線增加或減少 Pod 的副本數。
Deployment nginx-deployment 初始是兩個副本
修改nginx.yml
[root@k8s_master ~]# cat nginx.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 5
template:
metadata:
labels:
app: web_server
spec:
containers:
- name: nginx
image: nginx:1.7.9
[root@k8s_master ~]# kubectl apply -f nginx.yml
deployment "nginx-deployment" configured
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-deployment-2472058307-1lrwc 1/1 Running 0 19m
nginx-deployment-2472058307-6mj1s 1/1 Running 0 20s
nginx-deployment-2472058307-b5bp7 1/1 Running 0 20s
nginx-deployment-2472058307-n786m 1/1 Running 0 19m
nginx-deployment-2472058307-sthxc 1/1 Running 0 20s
redis 1/1 Running 3 4h
[root@k8s_master ~]# cat nginx.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
template:
metadata:
labels:
app: web_server
spec:
containers:
- name: nginx
image: nginx:1.7.9
[root@k8s_master ~]# kubectl apply -f nginx.yml
deployment "nginx-deployment" configured
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-deployment-2472058307-1lrwc 1/1 Running 0 21m
nginx-deployment-2472058307-b5bp7 1/1 Running 0 2m
nginx-deployment-2472058307-n786m 1/1 Running 0 21m
redis 1/1 Running 3 4h
4.4 Failover故障遷移
關閉一個節點
等待一段時間,Kubernetes會檢查到k8s-node2不可用,將k8s-node2上的Pod標記為Unknown狀態,並在 k8s-node1上新創建兩個 Pod,維持總副本數為 3
當 k8s-node2 恢復后,Unknown 的 Pod 會被刪除,不過已經運行的 Pod 不會重新調度回 k8s-node2。
4.5 用label控制Pod的位置
[root@k8s_master ~]# kubectl get nodes
NAME STATUS AGE
192.168.138.130 Ready 5h
192.168.138.161 Ready 5h
192.168.138.162 Ready 5h
[root@k8s_master ~]# kubectl get node --show-labels
NAME STATUS AGE LABELS
192.168.138.130 Ready 5h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.130
192.168.138.161 Ready 5h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.161
192.168.138.162 Ready 5h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.162
[root@k8s_master ~]# kubectl label node 192.168.138.162 disktype=ssd
node "192.168.138.162" labeled
[root@k8s_master ~]# kubectl get node --show-labels
NAME STATUS AGE LABELS
192.168.138.130 Ready 5h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.130
192.168.138.161 Ready 5h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.161
192.168.138.162 Ready 5h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/hostname=192.168.138.162
修改nginx.yml
[root@k8s_master ~]# cat nginx.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
template:
metadata:
labels:
app: web_server
spec:
containers:
- name: nginx
image: nginx:1.7.9
nodeSelector:
disktype: ssd
[root@k8s_master ~]# kubectl delete -f nginx.yml
deployment "nginx-deployment" deleted
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
redis 1/1 Running 3 4h
[root@k8s_master ~]# vim nginx.yml
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
redis 1/1 Running 3 4h 172.8.95.2 192.168.138.130
[root@k8s_master ~]# kubectl apply -f nginx.yml
deployment "nginx-deployment" created
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-deployment-1851826925-f0gpc 1/1 Running 0 10s 172.8.57.4 192.168.138.162
nginx-deployment-1851826925-phw8g 1/1 Running 0 10s 172.8.57.5 192.168.138.162
nginx-deployment-1851826925-ttsqm 1/1 Running 0 10s 172.8.57.3 192.168.138.162
redis 1/1 Running 3 4h 172.8.95.2 192.168.138.130
要刪除 label disktype,執行如下命令:
[root@k8s_master ~]# kubectl label node 192.168.138.162 disktype-
node "192.168.138.162" labeled
[root@k8s_master ~]# kubectl get node --show-labels
NAME STATUS AGE LABELS
192.168.138.130 Ready 6h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.130
192.168.138.161 Ready 6h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.161
192.168.138.162 Ready 6h beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=192.168.138.162
不過此時 Pod 並不會重新部署,依然在 192.168.138.162上運行
4.6 DaemonSet典型應用
因為部署原因,待補充;
Deployment 部署的副本 Pod 會分布在各個 Node 上,每個 Node 都可能運行好幾個副本。DaemonSet 的不同之處在於:每個 Node 上最多只能運行一個副本。
DaemonSet 的典型應用場景有:
在集群的每個節點上運行存儲 Daemon,比如 glusterd 或 ceph。
在每個節點上運行日志收集 Daemon,比如 flunentd 或 logstash。
在每個節點上運行監控 Daemon,比如 Prometheus Node Exporter 或 collectd。
其實 Kubernetes 自己就在用 DaemonSet 運行系統組件。執行如下命令:
[root@k8s_master ~]# kubectl get daemonset --namespace=kube-system
No resources found.
Kubernetes集群中每個當前運行的資源都可以通過kubectl edit查看其配置和運行狀態,比如 kubectl edit deployment nginx-deployment。
[root@k8s_master ~]# kubectl edit deployment nginx-deployment
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
annotations:
deployment.kubernetes.io/revision: "1"
kubectl.kubernetes.io/last-applied-configuration: '{"kind":"Deployment","apiVersion":"extensions/v1beta1","metadata":{"name":"nginx-deployment","creationTimestamp":null},"spec":{"replicas":3,"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"web_server"}},"spec":{"containers":[{"name":"nginx","image":"nginx:1.7.9","resources":{}}],"nodeSelector":{"disktype":"ssd"}}},"strategy":{}},"status":{}}'
creationTimestamp: 2018-08-30T15:11:23Z
generation: 1
labels:
app: web_server
name: nginx-deployment
namespace: default
resourceVersion: "36897"
selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/nginx-deployment
uid: f55b248b-ac66-11e8-97ec-000c29cc726b
spec:
replicas: 3
selector:
matchLabels:
app: web_server
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: web_server
spec:
containers:
- image: nginx:1.7.9
imagePullPolicy: IfNotPresent
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
dnsPolicy: ClusterFirst
nodeSelector:
disktype: ssd
restartPolicy: Always
securityContext: {}
terminationGracePeriodSeconds: 30
status:
availableReplicas: 3
conditions:
- lastTransitionTime: 2018-08-30T16:34:59Z
lastUpdateTime: 2018-08-30T16:34:59Z
message: Deployment has minimum availability.
reason: MinimumReplicasAvailable
status: "True"
type: Available
observedGeneration: 1
replicas: 3
updatedReplicas: 3
kind: deployment類型的資源。
containers 定義了nginx的容器。
status是當前deployment的運行時狀態,這個部分是 kubectl edit特有的。
[root@k8s_master ~]# cat prom.yml
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: node-exporter-daemonset
spec:
template:
metadata:
labels:
app: prometheus
spec:
hostNetwork: true #直接使用 Host 的網絡。
containers:
- name: node-exporter
image: prom/node-exporter
imagePullPolicy: IfNotPresent
command: #設置容器啟動命令。
- /bin/node_exporter
- --path.procfs
- /host/proc
- --path.sysfs
- /host/sys
- --collector.filesystem.ignored-mount-points
- ^/(sys|proc|dev|host|etc)($|/)
volumeMounts: #通過 Volume 將 Host 路徑 /proc、/sys 和 / 映射到容器中。
- name: proc
mountPath: /host/proc
- name: sys
mountPath: /host/sys
- name: root
mountPath: /rootfs
volumes:
- name: proc
hostPath:
path: /proc
- name: sys
hostPath :
path: /sys
- name: root
hostPath:
path: /
[root@k8s_master ~]# kubectl apply -f prom.yml
daemonset "node-exporter-daemonset" created
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-deployment-1851826925-f0gpc 1/1 Running 0 4h 172.8.57.4 192.168.138.162
nginx-deployment-1851826925-phw8g 1/1 Running 0 4h 172.8.57.5 192.168.138.162
nginx-deployment-1851826925-ttsqm 1/1 Running 0 4h 172.8.57.3 192.168.138.162
node-exporter-daemonset-0b3zt 1/1 Running 0 4m 192.168.138.130 192.168.138.130
node-exporter-daemonset-mr3qp 1/1 Running 0 4m 192.168.138.162 192.168.138.162
node-exporter-daemonset-xl4gb 1/1 Running 0 4m 192.168.138.161 192.168.138.161
redis 1/1 Running 3 9h 172.8.95.2 192.168.138.130
4.7 用k8s運行一次性任務
容器按照持續運行的時間可分為兩類:服務類容器和工作類容器。
服務類容器通常持續提供服務,需要一直運行,比如 http server,daemon 等。工作類容器則是一次性任務,比如批處理程序,完成后容器就退出。
Kubernetes的Deployment、ReplicaSet和DaemonSet都用於管理服務類容器;對於工作類容器,我們用 Job。
4.7.1 一次性job
[root@k8s_master ~]# cat myjob.yml
apiVersion: batch/v1 #batch/v1 是當前 Job 的 apiVersion。
kind: Job #指明當前資源的類型為 Job
metadata:
name: myjob
spec:
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
command: ["echo", "hello k8s job! "]
restartPolicy: Never
#restartPolicy 指定什么情況下需要重啟容器。對於Job,只能設置為Never或者OnFailure。對於其他controller(比如 Deployment)可以設置為 Always 。
[root@k8s_master ~]# kubectl apply -f myjob.yml
job "myjob" created
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-deployment-1851826925-f0gpc 1/1 Running 0 7h
nginx-deployment-1851826925-phw8g 1/1 Running 0 7h
nginx-deployment-1851826925-ttsqm 1/1 Running 0 7h
node-exporter-daemonset-0b3zt 1/1 Running 0 2h
node-exporter-daemonset-mr3qp 1/1 Running 0 2h
node-exporter-daemonset-xl4gb 1/1 Running 0 2h
redis 1/1 Running 3 11h
[root@k8s_master ~]# kubectl get job
NAME DESIRED SUCCESSFUL AGE
myjob 1 1 57s
DESIRED和SUCCESSFUL都為1,表示按照預期啟動了一個Pod,並且已經成功執行。kubectl get pod查看Pod的狀態
[root@k8s_master ~]# kubectl get pod --show-all
NAME READY STATUS RESTARTS AGE
myjob-24bwp 0/1 Completed 0 3m
nginx-deployment-1851826925-f0gpc 1/1 Running 0 7h
nginx-deployment-1851826925-phw8g 1/1 Running 0 7h
nginx-deployment-1851826925-ttsqm 1/1 Running 0 7h
node-exporter-daemonset-0b3zt 1/1 Running 0 2h
node-exporter-daemonset-mr3qp 1/1 Running 0 2h
node-exporter-daemonset-xl4gb 1/1 Running 0 2h
redis 1/1 Running 3 11h
因為Pod執行完畢后容器已經退出,需要用--show-all才能查看Completed狀態的Pod。
[root@k8s_master ~]# kubectl logs myjob-24bwp
hello k8s job!
[root@k8s_master ~]# kubectl delete -f myjob.yml
job "myjob" deleted
修改myjob.yml,故意引入一個錯誤:
[root@k8s_master ~]# cat myjob.yml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
command: ["invalidecho", "hello k8s job! "]
restartPolicy: Never
[root@k8s_master ~]# kubectl get job
NAME DESIRED SUCCESSFUL AGE
myjob 1 0 28s
[root@k8s_master ~]# kubectl get pod --show-all
NAME READY STATUS RESTARTS AGE
myjob-0p538 0/1 ContainerCreating 0 2s
myjob-76vb7 0/1 ContainerCannotRun 0 14s
myjob-8blcd 0/1 ContainerCannotRun 0 26s
myjob-8r379 0/1 ContainerCannotRun 0 32s
myjob-hd55m 0/1 ContainerCannotRun 0 39s
myjob-pwvk2 0/1 ContainerCannotRun 0 20s
myjob-qznph 0/1 ContainerCannotRun 0 8s
nginx-deployment-1851826925-f0gpc 1/1 Running 0 7h
nginx-deployment-1851826925-phw8g 1/1 Running 0 7h
nginx-deployment-1851826925-ttsqm 1/1 Running 0 7h
node-exporter-daemonset-0b3zt 1/1 Running 0 2h
node-exporter-daemonset-mr3qp 1/1 Running 0 2h
node-exporter-daemonset-xl4gb 1/1 Running 0 2h
redis 1/1 Running 3 12h
原因是:當第一個Pod啟動時,容器失敗退出,根據 restartPolicy: Never,此失敗容器不會被重啟,但Job DESIRED的Pod是1,目前SUCCESSFUL為0,不滿足,所以 Job controller會啟動新的 Pod,直到 SUCCESSFUL 為 1。對於我們這個例子,SUCCESSFUL 永遠也到不了1,所以 Job controller會一直創建新的Pod。為了終止這個行為,只能刪除Job。
[root@k8s_master ~]# kubectl delete -f myjob.yml
job "myjob" deleted
[root@k8s_master ~]# vim myjob.yml
[root@k8s_master ~]# cat myjob.yml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
command: ["invalidecho", "hello k8s job! "]
restartPolicy: OnFailure
[root@k8s_master ~]# kubectl get pod --show-all
NAME READY STATUS RESTARTS AGE
myjob-sccpv 0/1 CrashLoopBackOff 3 2m
nginx-deployment-1851826925-f0gpc 1/1 Running 0 7h
nginx-deployment-1851826925-phw8g 1/1 Running 0 7h
nginx-deployment-1851826925-ttsqm 1/1 Running 0 7h
node-exporter-daemonset-0b3zt 1/1 Running 0 3h
node-exporter-daemonset-mr3qp 1/1 Running 0 3h
node-exporter-daemonset-xl4gb 1/1 Running 0 3h
redis 1/1 Running 3 12h
這里只有一個Pod,不過RESTARTS為3,而且不斷增加,說明OnFailure生效,容器失敗后會自動重啟。
[root@k8s_master ~]# kubectl delete job myjob
job "myjob" deleted
[root@k8s_master ~]# kubectl get job
No resources found.
4.7.2 並行job
有時,我們希望能同時運行多個Pod,提高Job的執行效率。這個可以通過parallelism設置。這里我們將並行的Pod數量設置為2,實踐一下:
[root@k8s_master ~]# kubectl apply -f myjob.yml
job "myjob" created
[root@k8s_master ~]# cat myjob.yml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
parallelism: 2
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
command: ["echo", "hello k8s job! "]
restartPolicy: OnFailure
[root@k8s_master ~]# kubectl get job
NAME DESIRED SUCCESSFUL AGE
myjob <none> 2 1m
[root@k8s_master ~]# kubectl get pod --show-all
NAME READY STATUS RESTARTS AGE
myjob-0ll5n 0/1 Completed 0 1m
myjob-r8fjz 0/1 Completed 0 1m
nginx-deployment-1851826925-f0gpc 1/1 Running 0 7h
nginx-deployment-1851826925-phw8g 1/1 Running 0 7h
nginx-deployment-1851826925-ttsqm 1/1 Running 0 7h
node-exporter-daemonset-0b3zt 1/1 Running 0 3h
node-exporter-daemonset-mr3qp 1/1 Running 0 3h
node-exporter-daemonset-xl4gb 1/1 Running 0 3h
redis 1/1 Running 3 12h
我們還可以通過completions設置Job成功完成Pod的總數
上面配置的含義是:每次運行兩個Pod,直到總共有6個Pod成功完成。實踐一下
[root@k8s_master ~]# cat myjob.yml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
completions: 6
parallelism: 2
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
command: ["echo", "hello k8s job! "]
restartPolicy: OnFailure
[root@k8s_master ~]# kubectl apply -f myjob.yml
The Job "myjob" is invalid: spec.completions: Invalid value: 6: field is immutable
[root@k8s_master ~]# kubectl delete job myjob
job "myjob" deleted
[root@k8s_master ~]# kubectl apply -f myjob.yml
job "myjob" created
[root@k8s_master ~]# kubectl get pod --show-all
NAME READY STATUS RESTARTS AGE
myjob-0kll9 0/1 Completed 0 1m
myjob-35hgj 0/1 Completed 0 46s
myjob-cchqx 0/1 Completed 0 58s
myjob-jbmc1 0/1 Completed 0 48s
myjob-rjktd 0/1 Completed 0 1m
myjob-tpd9b 0/1 Completed 0 54s
nginx-deployment-1851826925-f0gpc 1/1 Running 0 7h
nginx-deployment-1851826925-phw8g 1/1 Running 0 7h
nginx-deployment-1851826925-ttsqm 1/1 Running 0 7h
node-exporter-daemonset-0b3zt 1/1 Running 0 3h
node-exporter-daemonset-mr3qp 1/1 Running 0 3h
node-exporter-daemonset-xl4gb 1/1 Running 0 3h
redis 1/1 Running 3 12h
[root@k8s_master ~]# kubectl get job
NAME DESIRED SUCCESSFUL AGE
myjob 6 6 1m
DESIRED和SUCCESSFUL均為6,符合預期。如果不指定completions和parallelism,默認值均為1。
4.7.3 定時job
Linux中有cron程序定時執行任務,Kubernetes的CronJob提供了類似的功能,可以定時執行Job。
[root@k8s_master ~]# cat cronjob.yml
apiVersion: batch/v2alpha1 #batch/v2alpha1 是當前 CronJob的apiVersion
kind: CronJob #指明當前資源的類型為 CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *" #schedule 指定什么時候運行 Job,其格式與 Linux cron一致。
jobTemplate: #jobTemplate定義Job的模板,格式與前面Job一致。
spec:
template:
spec:
containers:
- name: hello
image: busybox
command: ["echo", "hello k8s job! "]
restartPolicy: OnFailure
[root@k8s_master ~]# kubectl apply -f cronjob.yml
error: error validating "cronjob.yml": error validating data: couldn't find type: v2alpha1.CronJob; if you choose to ignore these errors, turn validation off with --validate=false
[root@k8s_master ~]# kubectl api-versions
apps/v1beta1
authentication.k8s.io/v1beta1
authorization.k8s.io/v1beta1
autoscaling/v1
batch/v1
certificates.k8s.io/v1alpha1
extensions/v1beta1
policy/v1beta1
rbac.authorization.k8s.io/v1alpha1
storage.k8s.io/v1beta1
v1
沒有batch/v2alpha1
[root@k8s_master ~]# kubectl version
Client Version: version.Info{Major:"1", Minor:"5", GitVersion:"v1.5.2", GitCommit:"269f928217957e7126dc87e6adfa82242bfe5b1e", GitTreeState:"clean", BuildDate:"2017-07-03T15:31:10Z", GoVersion:"go1.7.4", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"5", GitVersion:"v1.5.2", GitCommit:"269f928217957e7126dc87e6adfa82242bfe5b1e", GitTreeState:"clean", BuildDate:"2017-07-03T15:31:10Z", GoVersion:"go1.7.4", Compiler:"gc", Platform:"linux/amd64"}
[root@k8s_master ~]# cd /etc/kubernetes/
[root@k8s_master kubernetes]# cat apiserver
###
# kubernetes system config
#
# The following values are used to configure the kube-apiserver
#
# The address on the local server to listen to.
KUBE_API_ADDRESS="--insecure-bind-address=0.0.0.0"
# The port on the local server to listen on.
KUBE_API_PORT="--port=8080"
# Port minions listen on
# KUBELET_PORT="--kubelet-port=10250"
# Comma separated list of nodes in the etcd cluster
KUBE_ETCD_SERVERS="--etcd-servers=http://192.168.138.130:2379"
# Address range to use for services
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"
# default admission control policies
KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,ResourceQuota"
# Add your own!
KUBE_API_ARGS="--runtime-config=batch/v2alpha1=true"
[root@k8s_master ~]# for SERVICES in docker kube-apiserver kube-controller-manager kube-scheduler; do systemctl restart $SERVICES ; done
[root@k8s_master ~]# kubectl api-versions
apps/v1beta1
authentication.k8s.io/v1beta1
authorization.k8s.io/v1beta1
autoscaling/v1
batch/v1
batch/v2alpha1
certificates.k8s.io/v1alpha1
extensions/v1beta1
policy/v1beta1
rbac.authorization.k8s.io/v1alpha1
storage.k8s.io/v1beta1
v1
[root@k8s_master ~]# kubectl apply -f cronjob.yml
cronjob "hello" created
[root@k8s_master ~]# kubectl get cronjob
NAME SCHEDULE SUSPEND ACTIVE LAST-SCHEDULE
hello */1 * * * * False 0 Fri, 31 Aug 2018 07:38:00 +0800
[root@k8s_master ~]# kubectl get job
NAME DESIRED SUCCESSFUL AGE
hello-1535671980 1 1 6m
hello-1535672040 1 1 5m
hello-1535672100 1 1 4m
hello-1535672160 1 1 3m
hello-1535672220 1 1 2m
hello-1535672280 1 1 1m
hello-1535672340 1 1 18s
[root@k8s_master ~]# kubectl get pod --show-all
NAME READY STATUS RESTARTS AGE
hello-1535671980-t65n2 0/1 Completed 0 7m
hello-1535672040-grvbw 0/1 Completed 0 6m
hello-1535672100-0s9zw 0/1 Completed 0 5m
hello-1535672160-dbmkj 0/1 Completed 0 4m
hello-1535672220-v0lfr 0/1 Completed 0 3m
hello-1535672280-kq0cz 0/1 Completed 0 2m
hello-1535672340-9b2wz 0/1 Completed 0 1m
nginx-deployment-1851826925-f0gpc 1/1 Running 0 8h
nginx-deployment-1851826925-phw8g 1/1 Running 0 8h
nginx-deployment-1851826925-ttsqm 1/1 Running 0 8h
運行容器化應用是 Kubernetes 最重要的核心功能。為滿足不同的業務需要,Kubernetes提供了多種Controller,包括Deployment、DaemonSet、Job、CronJob等。
4.8 Service
我們不應該期望Kubernetes Pod 是健壯的,而是要假設Pod中的容器很可能因為各種原因發生故障而死掉。Deployment等controller 會通過動態創建和銷毀Pod來保證應用整體的健壯性。換句話說,Pod是脆弱的,但應用是健壯的。
每個 Pod 都有自己的 IP 地址。當 controller用新Pod替代發生故障的 Pod 時,新 Pod 會分配到新的 IP 地址。這樣就產生了一個問題:
如果一組 Pod 對外提供服務(比如 HTTP),它們的 IP 很有可能發生變化,那么客戶端如何找到並訪問這個服務呢?
Kubernetes 給出的解決方案是Service。
Kubernetes Service從邏輯上代表了一組Pod,具體是哪些Pod則是由label來挑選。Service有自己IP,而且這個IP是不變的。客戶端只需要訪問Service的 IP,Kubernetes 則負責建立和維護Service與Pod的映射關系。無論后端 Pod 如何變化,對客戶端不會有任何影響,因為 Service 沒有變。
4.8.1 Ip service
[root@k8s_master ~]# kubectl apply -f http.yml
deployment "httpd" created
[root@k8s_master ~]# cat http.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: httpd
spec:
replicas: 3
template:
metadata:
labels:
run: httpd
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
httpd-3937122862-00vn5 1/1 Running 0 8s
httpd-3937122862-7xbsp 1/1 Running 0 8s
httpd-3937122862-nkln3 1/1 Running 0 8s
node-exporter-daemonset-0b3zt 1/1 Running 1 5h
node-exporter-daemonset-mr3qp 1/1 Running 0 5h
node-exporter-daemonset-xl4gb 1/1 Running 0 5h
redis 1/1 Running 4 15h
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpd-3937122862-00vn5 1/1 Running 0 1m 172.8.18.2 192.168.138.161
httpd-3937122862-7xbsp 1/1 Running 0 1m 172.8.95.3 192.168.138.130
httpd-3937122862-nkln3 1/1 Running 0 1m 172.8.57.3 192.168.138.162
node-exporter-daemonset-0b3zt 1/1 Running 1 5h 192.168.138.130 192.168.138.130
node-exporter-daemonset-mr3qp 1/1 Running 0 5h 192.168.138.162 192.168.138.162
node-exporter-daemonset-xl4gb 1/1 Running 0 5h 192.168.138.161 192.168.138.161
redis 1/1 Running 4 15h 172.8.95.2 192.168.138.130
[root@k8s_master ~]# curl 172.8.18.2
<html><body><h1>It works!</h1></body></html>
[root@k8s_master ~]# curl 172.8.95.3
<html><body><h1>It works!</h1></body></html>
[root@k8s_master ~]# curl 172.8.57.3
<html><body><h1>It works!</h1></body></html>
[root@k8s_master ~]# cat httpd_sv.yml
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
type: NodePort
selector:
run: httpd
ports:
- protocol: TCP
port: 8082
targetPort: 80
[root@k8s_master ~]# kubectl apply -f httpd_sv.yml
service "httpd-svc" created
[root@k8s_master ~]# kubectl get service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc 10.254.192.170 <nodes> 8082:32304/TCP 21s
kubernetes 10.254.0.1 <none> 443/TCP 16h
[root@k8s_master ~]# curl 10.254.192.170:8082
<html><body><h1>It works!</h1></body></html>
httpd-svc分配到一個CLUSTER-IP 10.99.229.179。可以通過該 IP 訪問后端的 httpd Pod。
通過 kubectl describe 可以查看 httpd-svc 與 Pod 的對應關系
[root@k8s_master ~]# kubectl describe service httpd-svc
Name: httpd-svc
Namespace: default
Labels: <none>
Selector: run=httpd
Type: NodePort
IP: 10.254.192.170
Port: <unset> 8082/TCP
NodePort: <unset> 32304/TCP
Endpoints: 172.8.18.3:80,172.8.57.4:80,172.8.95.4:80
Session Affinity: None
No events.
[root@k8s_master ~]# iptables-save
可以通過iptables-save命令打印出當前節點的iptables規則,因為輸出較多,這里只截取與 httpd-svc Cluster IP10.99.229.179相關的信息:
這兩條規則的含義是:
如果 Cluster 內的 Pod(源地址來自 10.244.0.0/16)要訪問 httpd-svc,則允許。
其他源地址訪問 httpd-svc,跳轉到規則 KUBE-SVC-RL3JAE4GN7VOGDGP。
# Generated by iptables-save v1.4.21 on Fri Aug 31 09:39:39 2018
-A KUBE-SERVICES -d 10.254.192.170/32 -p tcp -m comment --comment "default/httpd-svc: cluster IP" -m tcp --dport 8082 -j KUBE-SVC-RL3JAE4GN7VOGDGP
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment "default/kubernetes:https" -j KUBE-SEP-JHVFJXYDAZENHIRG
KUBE-SVC-RL3JAE4GN7VOGDGP 規則如下:
1/3 的概率跳轉到規則 KUBE-SEP-C5KB52P4BBJQ35PH。
1/3 的概率(剩下 2/3 的一半)跳轉到規則 KUBE-SEP-HGVKQQZZCF7RV4IT。
1/3 的概率跳轉到規則 KUBE-SEP-XE25WGVXLHEIRVO5。
上面三個跳轉的規則如下:
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m comment --comment "default/httpd-svc:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-QOREOV6IV3RD2ZSR
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m comment --comment "default/httpd-svc:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-U44RF5KARTOM77DD
-A KUBE-SVC-RL3JAE4GN7VOGDGP -m comment --comment "default/httpd-svc:" -j KUBE-SEP-CAUY7XVTLK4ODWZ4
即將請求分別轉發到后端的三個 Pod。通過上面的分析,我們得到如下結論:
iptables 將訪問 Service 的流量轉發到后端 Pod,而且使用類似輪詢的負載均衡策略。
另外需要補充一點:Cluster 的每一個節點都配置了相同的 iptables 規則,這樣就確保了整個Cluster都能夠通過 Service的Cluster IP訪問Service。
4.8.2 K8s-dns
待補充
4.8.3 對外ip
除了 Cluster 內部可以訪問 Service,很多情況我們也希望應用的 Service 能夠暴露給 Cluster 外部。Kubernetes 提供了多種類型的 Service,默認是ClusterIP。
ClusterIP
Service 通過 Cluster 內部的 IP 對外提供服務,只有 Cluster 內的節點和 Pod 可訪問,這是默認的 Service 類型,前面實驗中的 Service 都是 ClusterIP。
NodePort
Service 通過 Cluster 節點的靜態端口對外提供服務。Cluster 外部可以通過 <NodeIP>:<NodePort> 訪問 Service。
LoadBalancer
Service 利用 cloud provider 特有的 load balancer 對外提供服務,cloud provider 負責將 load balancer 的流量導向 Service。目前支持的 cloud provider 有 GCP、AWS、Azur 等。
下面我們來實踐 NodePort,Service httpd-svc 的配置文件修改如下:
Kubernetes 依然會為 httpd-svc 分配一個 ClusterIP,不同的是:
EXTERNAL-IP 為 nodes,表示可通過 Cluster 每個節點自身的 IP 訪問 Service。
PORT(S)為 8082:32304。8082是 ClusterIP 監聽的端口,32304 則是節點上監聽的端口。Kubernetes會從30000-32767中分配一個可用的端口,每個節點都會監聽此端口並將請求轉發給Service。
[root@k8s_master ~]# cat httpd_sv.yml
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
type: NodePort
selector:
run: httpd
ports:
- protocol: TCP
port: 8082
targetPort: 80
[root@k8s_master ~]# kubectl apply -f httpd_sv.yml
service "httpd-svc" created
[root@k8s_master ~]# kubectl get service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc 10.254.192.170 <nodes> 8082:32304/TCP 46m
kubernetes 10.254.0.1 <none> 443/TCP 17h
[root@k8s_master ~]# netstat -lntup|grep 32304
tcp6 0 0 :::32304 :::* LISTEN 1138/kube-proxy
[root@k8s_master ~]# curl 192.168.138.130:32304
<html><body><h1>It works!</h1></body></html>
[root@k8s_master ~]# curl 192.168.138.161:32304
<html><body><h1>It works!</h1></body></html>
[root@k8s_master ~]# curl 192.168.138.162:32304
<html><body><h1>It works!</h1></body></html>
NodePort 默認是的隨機選擇,不過我們可以用 nodePort 指定某個特定端口。
[root@k8s_master ~]# cat httpd_sv.yml
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
type: NodePort
selector:
run: httpd
ports:
- protocol: TCP
nodePort: 32100
port: 8082
targetPort: 80
現在配置文件中就有三個 Port 了:
nodePort 是節點上監聽的端口。
port 是 ClusterIP 上監聽的端口。
targetPort 是 Pod 監聽的端口。
最終,Node和ClusterIP在各自端口上接收到的請求都會通過iptables轉發到Pod的targetPort。
[root@k8s_master ~]# kubectl apply -f httpd_sv.yml
service "httpd-svc" configured
[root@k8s_master ~]# netstat -lntup|grep 32100
tcp6 0 0 :::32100 :::* LISTEN 1138/kube-proxy
[root@k8s_master ~]# curl 192.168.138.130:32100
<html><body><h1>It works!</h1></body></html>
[root@k8s_master ~]# curl 192.168.138.161:32100
<html><body><h1>It works!</h1></body></html>
[root@k8s_master ~]# curl 192.168.138.162:32100
<html><body><h1>It works!</h1></body></html>
4.9 Rolling&&update
[root@k8s_master ~]# cat httpd.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: httpd
spec:
replicas: 2
template:
metadata:
labels:
run: httpd
spec:
containers:
- name: httpd
image: httpd
ports:
- containerPort: 80
[root@k8s_master ~]# kubectl apply -f httpd.yml
deployment "httpd" configured
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpd-3457006022-0bsbg 1/1 Running 0 2h 172.8.18.3 192.168.138.161
httpd-3457006022-932jz 1/1 Running 0 2h 172.8.57.4 192.168.138.162
node-exporter-daemonset-0b3zt 1/1 Running 1 8h 192.168.138.130 192.168.138.130
node-exporter-daemonset-mr3qp 1/1 Running 0 8h 192.168.138.162 192.168.138.162
node-exporter-daemonset-xl4gb 1/1 Running 0 8h 192.168.138.161 192.168.138.161
redis 1/1 Running 4 17h 172.8.95.2 192.168.138.130
[root@k8s_master ~]# cat httpd.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: httpd
spec:
replicas: 2
template:
metadata:
labels:
run: httpd
spec:
containers:
- name: httpd
image: httpd:2.2.31
ports:
- containerPort: 80
[root@k8s_master ~]# kubectl apply -f httpd.yml
deployment "httpd" configured
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpd-1478671140-c99t1 1/1 Running 0 5m 172.8.95.3 192.168.138.130
httpd-1478671140-zb8jc 1/1 Running 0 5m 172.8.18.2 192.168.138.161
[root@k8s_master ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f7314640b4c5 httpd:2.2.31 "httpd-foreground" 5 minutes ago Up 5 minutes k8s_httpd.1cd1feb7_httpd-1478671140-c99t1_default_65fc99ab-acd1-11e8-a26f-000c29cc726b_f457a097
[root@k8s_master ~]# kubectl get replicaset -o wide
NAME DESIRED CURRENT READY AGE CONTAINER(S) IMAGE(S) SELECTOR
httpd-1478671140 2 2 2 8m httpd httpd:2.2.31 pod-template-hash=1478671140,run=httpd
httpd-3457006022 0 0 0 2h httpd httpd pod-template-hash=3457006022,run=httpd
httpd-3510483496 0 0 0 2h httpd httpd pod-template-hash=3510483496,run=httpd
httpd-3937122862 0 0 0 2h httpd httpd pod-template-hash=3937122862,run=httpd
部署過程如下:
創建Deployment httpd
創建ReplicaSet httpd-1478671140
創建三個 Pod
當前鏡像為 httpd
將配置文件中httpd替換為 httpd:2.2.31,再次執行kubectl apply。
Deployment httpd 的鏡像更新為 httpd:2.2.31
新創建了 ReplicaSet httpd-1276601241,鏡像為 httpd:2.2.31,並且管理了三個新的 Pod。
之前的 ReplicaSet httpd-551879778 里面已經沒有任何 Pod。
結論是:ReplicaSet httpd-551879778 的三個 httpd Pod 已經被 ReplicaSet httpd-1276601241 的三個 httpd:2.2.31 Pod 替換了。
具體過程可以通過 kubectl describe deployment httpd 查看
[root@k8s_master ~]# kubectl describe deployment httpd
Name: httpd
Namespace: default
CreationTimestamp: Fri, 31 Aug 2018 09:20:11 +0800
Labels: run=httpd
Selector: run=httpd
Replicas: 2 updated | 2 total | 2 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 1 max unavailable, 1 max surge
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
OldReplicaSets: <none>
NewReplicaSet: httpd-1478671140 (2/2 replicas created)
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
2h 12m 2 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set httpd-3457006022 to 2
11m 11m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set httpd-1478671140 to 1
11m 11m 1 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set httpd-3457006022 to 1
11m 11m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set httpd-1478671140 to 2
10m 10m 1 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set httpd-3457006022 to 0
每次只更新替換一個 Pod:
ReplicaSet httpd-1276601241 增加一個 Pod,總數為 1。
ReplicaSet httpd-551879778 減少一個 Pod,總數為 2。
ReplicaSet httpd-1276601241 增加一個 Pod,總數為 2。
ReplicaSet httpd-551879778 減少一個 Pod,總數為 1。
ReplicaSet httpd-1276601241 增加一個 Pod,總數為 3。
ReplicaSet httpd-551879778 減少一個 Pod,總數為 0。
每次替換的 Pod 數量是可以定制的。Kubernetes 提供了兩個參數 maxSurge 和 maxUnavailable 來精細控制 Pod 的替換數量,我們將在后面結合 Health Check 特性一起討論。
金絲雀 把不同版本放到各自的deployment,通過同一個service訪問。具體例子https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments
https://www.jianshu.com/p/022685baba7d
記錄回滾
--record的作用是將當前命令記錄到revision記錄中,這樣我們就可以知道每個revison對應的是哪個配置文件。通過 kubectl rollout history deployment httpd 查看revison歷史記錄。
[root@k8s_master ~]# kubectl apply -f http2.yml --record
deployment "httpd" configured
[root@k8s_master ~]# kubectl apply -f http.yml --record
error: the path "http.yml" does not exist
[root@k8s_master ~]# kubectl apply -f httpd.yml --record
deployment "httpd" configured
[root@k8s_master ~]# kubectl rollout history deployment httpd
deployments "httpd"
REVISION CHANGE-CAUSE
1 <none>
3 <none>
6 kubectl apply -f http2.yml --record
7 kubectl apply -f httpd.yml --record
CHANGE-CAUSE就是--record 的結果。如果要回滾到某個版本,比如revision 1,可以執行命令 kubectl rollout undo deployment httpd --to-revision=1:
[root@k8s_master ~]# kubectl rollout undo deployment httpd --to-revision=6
deployment "httpd" rolled back
此時revison歷史記錄也會發生相應變化。
Revison7變成了revison8。不過我們可以通過 CHANGE-CAUSE 知道每個 revison 的具體含義。所以一定要在執行 kubectl apply 時加上 --record參數。
[root@k8s_master ~]# kubectl rollout history deployment httpd
deployments "httpd"
REVISION CHANGE-CAUSE
1 <none>
3 <none>
7 kubectl apply -f httpd.yml --record
8 kubectl apply -f http2.yml --record
4.10 Health check
強大的自愈能力是Kubernetes 這類容器編排引擎的一個重要特性。自愈的默認實現方式是自動重啟發生故障的容器。除此之外,用戶還可以利用Liveness 和Readiness 探測機制設置更精細的健康檢查,進而實現如下需求:
零停機部署。
避免部署無效的鏡像。
更加安全的滾動升級。
下面通過實踐學習 Kubernetes 的 Health Check 功能。
我們首先學習Kubernetes默認的健康檢查機制:
每個容器啟動時都會執行一個進程,此進程由Dockerfile的CMD或ENTRYPOINT 指定。
如果進程退出時返回碼非零,則認為容器發生故障,Kubernetes就會根據restartPolicy重啟容器。
下面我們模擬一個容器發生故障的場景,Pod 配置文件如下:
[root@k8s_master ~]# cat healthcheck.yml
apiVersion: v1
kind: Pod
metadata:
labels:
test: healthcheck
name: healthcheck
spec:
restartPolicy: OnFailure
containers:
- name: healthcheck
image: busybox
args:
- /bin/sh
- -c
- sleep 10; exit 1
[root@k8s_master ~]# kubectl apply -f healthcheck.yml --record
pod "healthcheck" created
Pod 的 restartPolicy 設置為 OnFailure,默認為 Always。
sleep 10; exit 1模擬容器啟動10秒后發生故障。
執行kubectl apply創建 Pod,命名為healthcheck。
[root@k8s_master ~]# kubectl get pod healthcheck
NAME READY STATUS RESTARTS AGE
healthcheck 0/1 Error 3 1m
在上面的例子中,容器進程返回值非零,Kubernetes則認為容器發生故障,需要重啟。
但有不少情況是發生了故障,但進程並不會退出。
比如訪問Web服務器時顯示500內部錯誤,可能是系統超載,也可能是資源死鎖,此時httpd進程並沒有異常退出,在這種情況下重啟容器可能是最直接最有效的解決方案,那我們如何利用Health Check機制來處理這類場景呢?
4.10.1 lineness
Liveness探測讓用戶可以自定義判斷容器是否健康的條件。如果探測失敗,Kubernetes就會重啟容器。
還是舉例說明,創建如下Pod:
[root@k8s_master ~]# cat liveness.yml
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness
spec:
restartPolicy: OnFailure
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
啟動進程首先創建文件 /tmp/healthy,30 秒后刪除,在我們的設定中,如果 /tmp/healthy 文件存在,則認為容器處於正常狀態,反正則發生故障。
livenessProbe 部分定義如何執行 Liveness 探測:
探測的方法是:通過 cat 命令檢查 /tmp/healthy 文件是否存在。如果命令執行成功,返回值為零,Kubernetes 則認為本次 Liveness 探測成功;如果命令返回值非零,本次 Liveness 探測失敗。
initialDelaySeconds: 10 指定容器啟動 10 之后開始執行 Liveness 探測,我們一般會根據應用啟動的准備時間來設置。比如某個應用正常啟動要花 30 秒,那么 initialDelaySeconds 的值就應該大於 30。
periodSeconds: 5 指定每 5 秒執行一次 Liveness 探測。Kubernetes 如果連續執行 3 次 Liveness 探測均失敗,則會殺掉並重啟容器。
下面創建 Pod liveness:
[root@k8s_master ~]# kubectl apply -f liveness.yml
pod "liveness" created
從配置文件可知,最開始的30秒,/tmp/healthy存在,cat命令返回0,Liveness探測成功,這段時間 kubectl describe pod liveness的Events部分會顯示正常的日志。
[root@k8s_master ~]# kubectl describe pod liveness
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
1m 1m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Pulling pulling image "busybox"
1m 1m 2 {kubelet 192.168.138.161} Warning MissingClusterDNS kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.
1m 1m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Pulled Successfully pulled image "busybox"
1m 1m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Created Created container with docker id d86661d05d5d; Security:[seccomp=unconfined]
1m 1m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Started Started container with docker id d86661d05d5d
27s 27s 1 {default-scheduler } Normal Scheduled Successfully assigned liveness to 192.168.138.161
35秒之后,日志會顯示/tmp/healthy已經不存在,Liveness 探測失敗。再過幾十秒,幾次探測都失敗后,容器會被重啟。
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
2m 2m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Created Created container with docker id d86661d05d5d; Security:[seccomp=unconfined]
2m 2m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Started Started container with docker id d86661d05d5d
1m 1m 3 {kubelet 192.168.138.161} spec.containers{liveness} Warning Unhealthy Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
1m 1m 1 {default-scheduler } Normal Scheduled Successfully assigned liveness to 192.168.138.161
2m 1m 2 {kubelet 192.168.138.161} spec.containers{liveness} Normal Pulling pulling image "busybox"
1m 1m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Killing Killing container with docker id d86661d05d5d: pod "liveness_default(415f53a6-acdd-11e8-a26f-000c29cc726b)" container "liveness" is unhealthy, it will be killed and re-created.
2m 1m 2 {kubelet 192.168.138.161} spec.containers{liveness} Normal Pulled Successfully pulled image "busybox"
2m 1m 3 {kubelet 192.168.138.161} Warning MissingClusterDNS kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.
1m 1m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Created Created container with docker id d166192948c7; Security:[seccomp=unconfined]
1m 1m 1 {kubelet 192.168.138.161} spec.containers{liveness} Normal Started Started container with docker id d166192948c7
[root@k8s_master ~]# kubectl get pod liveness
NAME READY STATUS RESTARTS AGE
liveness 1/1 Running 3 4m
4.10.2 readiness
用戶通過Liveness探測可以告訴Kubernetes什么時候通過重啟容器實現自愈;Readiness 探測則是告訴 Kubernetes什么時候可以將容器加入到Service負載均衡池中,對外提供服務。
Readiness探測的配置語法與Liveness探測完全一樣,下面是個例子:
[root@k8s_master ~]# cat readiness.yml
apiVersion: v1
kind: Pod
metadata:
labels:
test: readiness
name: readiness
spec:
restartPolicy: OnFailure
containers:
- name: readiness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
[root@k8s_master ~]# kubectl apply -f readiness.yml
pod "readiness" created
[root@k8s_master ~]# kubectl get pod readiness
NAME READY STATUS RESTARTS AGE
readiness 1/1 Running 0 47s
[root@k8s_master ~]# kubectl get pod readiness
NAME READY STATUS RESTARTS AGE
readiness 0/1 Running 0 54s
Pod readiness 的 READY 狀態經歷了如下變化:
剛被創建時,READY 狀態為不可用。
15 秒后(initialDelaySeconds + periodSeconds),第一次進行 Readiness 探測並成功返回,設置 READY 為可用。
30 秒后,/tmp/healthy 被刪除,連續 3 次 Readiness 探測均失敗后,READY 被設置為不可用。
[root@k8s_master ~]# kubectl describe pod readiness
Name: readiness
Namespace: default
Node: 192.168.138.130/192.168.138.130
Start Time: Fri, 31 Aug 2018 13:32:25 +0800
Labels: test=readiness
Status: Running
IP: 172.8.95.4
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
2m 2m 1 {default-scheduler } Normal Scheduled Successfully assigned readiness to 192.168.138.130
2m 2m 1 {kubelet 192.168.138.130} spec.containers{readiness} Normal Pulling pulling image "busybox"
2m 2m 2 {kubelet 192.168.138.130} Warning MissingClusterDNS kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.
2m 2m 1 {kubelet 192.168.138.130} spec.containers{readiness} Normal Pulled Successfully pulled image "busybox"
2m 2m 1 {kubelet 192.168.138.130} spec.containers{readiness} Normal Created Created container with docker id 265ac2f4e37a; Security:[seccomp=unconfined]
2m 2m 1 {kubelet 192.168.138.130} spec.containers{readiness} Normal Started Started container with docker id 265ac2f4e37a
1m 2s 19 {kubelet 192.168.138.130} spec.containers{readiness} Warning Unhealthy Readiness probe failed: cat: can't open '/tmp/healthy': No such file or directory
Liveness探測和Readiness探測是兩種 Health Check 機制,如果不特意配置,Kubernetes 將對兩種探測采取相同的默認行為,即通過判斷容器啟動進程的返回值是否為零來判斷探測是否成功。
兩種探測的配置方法完全一樣,支持的配置參數也一樣。不同之處在於探測失敗后的行為:Liveness 探測是重啟容器;Readiness 探測則是將容器設置為不可用,不接收 Service 轉發的請求。
Liveness 探測和 Readiness 探測是獨立執行的,二者之間沒有依賴,所以可以單獨使用,也可以同時使用。用 Liveness 探測判斷容器是否需要重啟以實現自愈;用 Readiness 探測判斷容器是否已經准備好對外提供服務
readiness的作用是有些服務需要一段時間的預熱,對於這種服務,如果不設置readiness,就永遠不可用
4.10.3 scale up && health
對於多副本應用,當執行 Scale Up 操作時,新副本會作為 backend 被添加到 Service 的負責均衡中,與已有副本一起處理客戶的請求。考慮到應用啟動通常都需要一個准備階段,比如加載緩存數據,連接數據庫等,從容器啟動到正真能夠提供服務是需要一段時間的。我們可以通過 Readiness探測判斷容器是否就緒,避免將請求發送到還沒有 ready 的 backend。
[root@k8s_master ~]# kubectl apply -f httpd_health.yml --record
deployment "httpd" created
service "httpd-svc" created
[root@k8s_master ~]# cat httpd_health.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: httpd
spec:
replicas: 3
template:
metadata:
labels:
run: httpd
spec:
containers:
- name: httpd
image: httpd:2.2.31
ports:
- containerPort: 80
readinessProbe:
httpGet:
scheme: HTTP
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
type: NodePort
selector:
run: httpd
ports:
- protocol: TCP
nodePort: 30000
port: 8082
targetPort: 80
上面配置的作用是:
容器啟動 10 秒之后開始探測。
如果 http://[container_ip]:8080/healthy 返回代碼不是 200-400,表示容器沒有就緒,不接收 Service web-svc 的請求。
每隔 5 秒再探測一次。
直到返回代碼為 200-400,表明容器已經就緒,然后將其加入到 web-svc 的負責均衡中,開始處理客戶請求。
探測會繼續以 5 秒的間隔執行,如果連續發生 3 次失敗,容器又會從負載均衡中移除,直到下次探測成功重新加入。
對於生產環境中重要的應用都建議配置 Health Check,保證處理客戶請求的容器都是准備就緒的 Service backend。
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
healthcheck 0/1 CrashLoopBackOff 14 54m 172.8.95.3 192.168.138.130
httpd-1649560772-0ggrh 1/1 Running 0 2m 172.8.18.3 192.168.138.161
httpd-1649560772-qm2vf 1/1 Running 0 2m 172.8.57.3 192.168.138.162
httpd-1649560772-r26zv 1/1 Running 0 2m 172.8.95.4 192.168.138.130
liveness 1/1 Running 15 41m 172.8.18.2 192.168.138.161
node-exporter-daemonset-0b3zt 1/1 Running 1 10h 192.168.138.130 192.168.138.130
[root@k8s_master ~]# kubectl describe svc httpd-svc
Name: httpd-svc
Namespace: default
Labels: <none>
Selector: run=httpd
Type: NodePort
IP: 10.254.76.56
Port: <unset> 8082/TCP
NodePort: <unset> 30000/TCP
Endpoints: 172.8.18.3:80,172.8.57.3:80,172.8.95.4:80
Session Affinity: None
No events.
4.10.4 在滾動更新中使用
現有一個正常運行的多副本應用,接下來對應用進行更新(比如使用更高版本的 image),Kubernetes 會啟動新副本,然后發生了如下事件:
正常情況下新副本需要 10 秒鍾完成准備工作,在此之前無法響應業務請求。
但由於人為配置錯誤,副本始終無法完成准備工作(比如無法連接后端數據庫)。
先別繼續往下看,現在請花一分鍾思考這個問題:如果沒有配置 Health Check,會出現怎樣的情況?
因為新副本本身沒有異常退出,默認的 Health Check 機制會認為容器已經就緒,進而會逐步用新副本替換現有副本,其結果就是:當所有舊副本都被替換后,整個應用將無法處理請求,無法對外提供服務。如果這是發生在重要的生產系統上,后果會非常嚴重。
如果正確配置了 Health Check,新副本只有通過了 Readiness 探測,才會被添加到 Service;如果沒有通過探測,現有副本不會被全部替換,業務仍然正常進行。
下面通過例子來實踐 Health Check 在 Rolling Update 中的應用。
用如下配置文件 app.v1.yml 模擬一個 10 副本的應用:
[root@k8s_master ~]# kubectl apply -f app.v1.yml --record
deployment "app" created
[root@k8s_master ~]# kubectl get deployment app
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
app 10 10 10 0 10s
[root@k8s_master ~]# kubectl get deployment app
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
app 10 10 10 10 47s
[root@k8s_master ~]# cat app.v1.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: app
spec:
replicas: 10
template:
metadata:
labels:
run: app
spec:
containers:
- name: app
image: busybox
args:
- /bin/sh
- -c
- sleep 10; touch /tmp/healthy; sleep 30000
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
app-3748071646-0fsct 1/1 Running 0 3m
app-3748071646-0gdm1 1/1 Running 0 3m
app-3748071646-1ftd4 1/1 Running 0 3m
app-3748071646-242cv 1/1 Running 0 3m
app-3748071646-281v1 1/1 Running 0 3m
app-3748071646-7j3wg 1/1 Running 0 3m
app-3748071646-8dd35 1/1 Running 0 3m
app-3748071646-msq6v 1/1 Running 0 3m
app-3748071646-rd17j 1/1 Running 0 3m
app-3748071646-xqqjk 1/1 Running 0 3m
[root@k8s_master ~]# kubectl get deployment app
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
app 10 10 10 10 37s
很顯然,由於新副本中不存在 /tmp/healthy,是無法通過 Readiness 探測的。驗證如下:
[root@k8s_master ~]# kubectl apply -f app.v2.yml --record
deployment "app" configured
[root@k8s_master ~]# cat app.v2.yml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: app
spec:
replicas: 10
template:
metadata:
labels:
run: app
spec:
containers:
- name: app
image: busybox
args:
- /bin/sh
- -c
- sleep 30000
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
[root@k8s_master ~]# kubectl get deployment app
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
app 10 11 2 9 57s
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
app-2073951902-2wld1 0/1 Running 0 21s
app-2073951902-mqw85 0/1 Running 0 21s
app-3748071646-3lk5z 1/1 Running 0 1m
app-3748071646-548ww 1/1 Terminating 0 1m
app-3748071646-fj9zz 1/1 Running 0 1m
app-3748071646-kh4xl 1/1 Running 0 1m
app-3748071646-nxrdh 1/1 Running 0 1m
app-3748071646-p5p86 1/1 Running 0 1m
app-3748071646-pcrww 1/1 Running 0 1m
app-3748071646-pwd2r 1/1 Running 0 1m
app-3748071646-sgqzx 1/1 Running 0 1m
一直保持
[root@k8s_master ~]# kubectl get deployment app
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
app 10 11 2 9 4m
很顯然,由於新副本中不存在 /tmp/healthy,是無法通過 Readiness 探測的。驗證如下:
這個截圖包含了大量的信息,值得我們詳細分析。
先關注 kubectl get pod 輸出:
從 Pod 的 AGE 欄可判斷,最后 5 個 Pod 是新副本,目前處於 NOT READY 狀態。
舊副本從最初 10 個減少到 8 個。
再來看 kubectl get deployment app 的輸出:
DESIRED 10 表示期望的狀態是 10 個 READY 的副本。
CURRENT 13 表示當前副本的總數:即 8 個舊副本 + 5 個新副本。
UP-TO-DATE 5 表示當前已經完成更新的副本數:即 5 個新副本。
AVAILABLE 8 表示當前處於 READY 狀態的副本數:即 8個舊副本。
在我們的設定中,新副本始終都無法通過 Readiness 探測,所以這個狀態會一直保持下去。
上面我們模擬了一個滾動更新失敗的場景。不過幸運的是:Health Check 幫我們屏蔽了有缺陷的副本,同時保留了大部分舊副本,業務沒有因更新失敗受到影響。
接下來我們要回答:為什么新創建的副本數是 5 個,同時只銷毀了 2 個舊副本?
原因是:滾動更新通過參數 maxSurge 和 maxUnavailable 來控制副本替換的數量。
maxSurge
此參數控制滾動更新過程中副本總數的超過 DESIRED 的上限。maxSurge 可以是具體的整數(比如 3),也可以是百分百,向上取整。maxSurge 默認值為 25%。
在上面的例子中,DESIRED 為 10,那么副本總數的最大值為:
roundUp(10 + 10 * 25%) = 13
所以我們看到 CURRENT 就是 13。
maxUnavailable
此參數控制滾動更新過程中,不可用的副本相占 DESIRED 的最大比例。 maxUnavailable 可以是具體的整數(比如 3),也可以是百分百,向下取整。maxUnavailable 默認值為 25%。
在上面的例子中,DESIRED 為 10,那么可用的副本數至少要為:
10 - roundDown(10 * 25%) = 8
所以我們看到 AVAILABLE 就是 8。
maxSurge 值越大,初始創建的新副本數量就越多;maxUnavailable 值越大,初始銷毀的舊副本數量就越多。
理想情況下,我們這個案例滾動更新的過程應該是這樣的:
首先創建 3 個新副本使副本總數達到 13 個。
然后銷毀 2 個舊副本使可用的副本數降到 8 個。
當這 2 個舊副本成功銷毀后,可再創建 2 個新副本,使副本總數保持為 13 個。
當新副本通過 Readiness 探測后,會使可用副本數增加,超過 8。
進而可以繼續銷毀更多的舊副本,使可用副本數回到 8。
舊副本的銷毀使副本總數低於 13,這樣就允許創建更多的新副本。
這個過程會持續進行,最終所有的舊副本都會被新副本替換,滾動更新完成。
而我們的實際情況是在第 4 步就卡住了,新副本無法通過 Readiness 探測。這個過程可以在 kubectl describe deployment app 的日志部分查看。
[root@k8s_master ~]# kubectl describe deployment app
Name: app
Namespace: default
CreationTimestamp: Fri, 31 Aug 2018 14:31:26 +0800
Labels: run=app
Selector: run=app
Replicas: 2 updated | 10 total | 9 available | 2 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 1 max unavailable, 1 max surge
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
OldReplicaSets: app-3748071646 (9/9 replicas created)
NewReplicaSet: app-2073951902 (2/2 replicas created)
Events:
FirstSeen LastSeen Count From SubObjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
9m 9m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set app-3748071646 to 10
9m 9m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set app-2073951902 to 1
9m 9m 1 {deployment-controller } Normal ScalingReplicaSet Scaled down replica set app-3748071646 to 9
9m 9m 1 {deployment-controller } Normal ScalingReplicaSet Scaled up replica set app-2073951902 to 2
如果滾動更新失敗,可以通過 kubectl rollout undo 回滾到上一個版本
[root@k8s_master ~]# kubectl rollout history deployment app
deployments "app"
REVISION CHANGE-CAUSE
1 kubectl apply -f app.v1.yml --record
2 kubectl apply -f app.v2.yml –record
[root@k8s_master ~]# kubectl rollout undo deployment app --to-revision=1
deployment "app" rolled back
[root@k8s_master ~]# kubectl get deployment app
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
app 10 10 10 10 13m
如果要定制 maxSurge 和 maxUnavailable,可以如下配置:
4.11 數據管理
首先我們會學習Volume,以及 Kubernetes 如何通過 Volume 為集群中的容器提供存儲;然后我們會實踐幾種常用的Volume 類型並理解它們各自的應用場景;最后,我們會討論Kubernetes如何通過Persistent Volume和Persistent Volume Claim分離集群管理員與集群用戶的職責,並實踐 Volume 的靜態供給和動態供給。
本節我們討論 Kubernetes 的存儲模型 Volume,學習如何將各種持久化存儲映射到容器。
我們經常會說:容器和 Pod 是短暫的。
其含義是它們的生命周期可能很短,會被頻繁地銷毀和創建。容器銷毀時,保存在容器內部文件系統中的數據都會被清除。
為了持久化保存容器的數據,可以使用 Kubernetes Volume。
Volume的生命周期獨立於容器,Pod 中的容器可能被銷毀和重建,但 Volume 會被保留。
本質上,Kubernetes Volume 是一個目錄,這一點與 Docker Volume 類似。當 Volume 被 mount 到 Pod,Pod 中的所有容器都可以訪問這個 Volume。Kubernetes Volume 也支持多種 backend 類型,包括 emptyDir、hostPath、GCE Persistent Disk、AWS Elastic Block Store、NFS、Ceph 等,完整列表可參考 https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes
Volume 提供了對各種 backend 的抽象,容器在使用 Volume 讀寫數據的時候不需要關心數據到底是存放在本地節點的文件系統中呢還是雲硬盤上。對它來說,所有類型的 Volume 都只是一個目錄。
我們將從最簡單的 emptyDir 開始學習 Kubernetes Volume。
emptyDir 是最基礎的 Volume 類型。正如其名字所示,一個 emptyDir Volume 是 Host 上的一個空目錄。
emptyDir Volume 對於容器來說是持久的,對於 Pod 則不是。當 Pod 從節點刪除時,Volume 的內容也會被刪除。但如果只是容器被銷毀而 Pod 還在,則 Volume 不受影響。
也就是說:emptyDir Volume 的生命周期與 Pod 一致。
Pod 中的所有容器都可以共享 Volume,它們可以指定各自的mount路徑。下面通過例子來實踐emptyDir,配置文件如下
[root@k8s_master ~]# cat volume.yml
apiVersion: v1
kind: Pod
metadata:
name: producer-consumer
spec:
containers:
- image: busybox
name: producer
volumeMounts:
- mountPath: /producer_dir
name: shared-volume
args:
- /bin/sh
- -c
- echo "hello world" > /producer_dir/hello; sleep 30000
- image: busybox
name: consumer
volumeMounts:
- mountPath: /consumer_dir
name: shared-volume
args:
- /bin/sh
- -c
- cat /consumer_dir/hello; sleep 30000
volumes:
- name: shared-volume
emptyDir: {}
這里我們模擬了一個 producer-consumer 場景。Pod 有兩個容器 producer和 consumer,它們共享一個 Volume。producer 負責往 Volume 中寫數據,consumer 則是從 Volume 讀取數據。
文件最底部 volumes 定義了一個 emptyDir 類型的 Volume shared-volume。
producer 容器將 shared-volume mount 到 /producer_dir 目錄。
producer 通過 echo 將數據寫到文件 hello 里。
consumer 容器將 shared-volume mount 到 /consumer_dir 目錄。
consumer 通過 cat 從文件 hello 讀數據。
執行如下命令創建 Pod:
[root@k8s_master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
producer-consumer 2/2 Running 0 21s
[root@k8s_master ~]# kubectl logs producer-consumer consumer
hello world
kubectl logs 顯示容器 consumer 成功讀到了 producer 寫入的數據,驗證了兩個容器共享 emptyDir Volume。
因為 emptyDir 是 Docker Host 文件系統里的目錄,其效果相當於執行了 docker run -v /producer_dir 和 docker run -v /consumer_dir。通過 docker inspect 查看容器的詳細配置信息,我們發現兩個容器都 mount 了同一個目錄:
Docker inspect 容器id1
"Mounts": [
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/d37d1a69-aced-11e8-a26f-000c29cc726b/volumes/kubernetes.io~empty-dir/shared-volume",
"Destination": "/consumer_dir",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
Docker inspect 容器id1
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/d37d1a69-aced-11e8-a26f-000c29cc726b/volumes/kubernetes.io~empty-dir/shared-volume",
"Destination": "/producer_dir",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
Hostpath
[root@k8s_master ~]# cat webserver.yml
apiVersion: v1
kind: Pod
metadata:
name: test-pd
spec:
containers:
- image: test-webserver:123
name: test-container
volumeMounts:
- mountPath: /test-pd
name: test-volume
volumes:
- name: test-volume
hostPath:
# directory location on host
path: /data
docker ps
gcr.io/google_containers/test-webserver latest 25906c5a72ed 3 years ago 4.53 MB
test-webserver 123 25906c5a72ed 3 years ago 4.53 MB
google鏡像默認要下載,更改名稱就可以了
[root@k8s_master ~]# kubectl apply -f webserver.yml
pod "test-pd" created
[root@k8s_master ~]# kubectl describe pod test-pd
Name: test-pd
Namespace: default
Node: 192.168.138.162/192.168.138.162
Start Time: Fri, 31 Aug 2018 18:14:57 +0800
Labels: <none>
Status: Running
IP: 172.8.57.4
Controllers: <none>
Containers:
test-container:
Container ID: docker://bb0bd6782853a168a7cac045ebc5986a39f9669a5923e847f751b79d46d25729
Image: test-webserver:123
Image ID: docker://sha256:25906c5a72eda6a5007c54bc9bd3ec39e446ef2289c95deb3abe53a0e115e0f0
Port:
State: Running
Started: Fri, 31 Aug 2018 18:14:58 +0800
Ready: True
Restart Count: 0
Volume Mounts:
/test-pd from test-volume (rw)
[root@k8s_master ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
healthcheck 0/1 CrashLoopBackOff 65 5h
test-pd 1/1 Running 0 9m
[root@k8s_client2 data]# curl 172.8.57.4
<pre>
<a href="test-pd/">test-pd/</a>
<a href="run/">run/</a>
<a href="etc/">etc/</a>
<a href="dev/">dev/</a>
<a href="sys/">sys/</a>
<a href="proc/">proc/</a>
<a href=".dockerenv">.dockerenv</a>
<a href="test-webserver">test-webserver</a>
</pre>
[root@k8s_client2 data]# curl 172.8.57.4/test-pd/1.html
houpj
[root@k8s_client2 data]# cat 1.html
Houpj
Ceph、GlusterFS 后續補充
4.12 PV&&PVC
Volume 提供了非常好的數據持久化方案,不過在可管理性上還有不足。
拿前面 AWS EBS 的例子來說,要使用 Volume,Pod 必須事先知道如下信息:
當前 Volume 來自 AWS EBS。
EBS Volume 已經提前創建,並且知道確切的 volume-id。
Pod 通常是由應用的開發人員維護,而 Volume 則通常是由存儲系統的管理員維護。開發人員要獲得上面的信息:
要么詢問管理員。
要么自己就是管理員。
這樣就帶來一個管理上的問題:應用開發人員和系統管理員的職責耦合在一起了。如果系統規模較小或者對於開發環境這樣的情況還可以接受。但當集群規模變大,特別是對於生成環境,考慮到效率和安全性,這就成了必須要解決的問題。
Kubernetes 給出的解決方案是 PersistentVolume 和 PersistentVolumeClaim。[pə'zɪstənt][klem]
PersistentVolume (PV) 是外部存儲系統中的一塊存儲空間,由管理員創建和維護。與 Volume 一樣,PV 具有持久性,生命周期獨立於 Pod。
PersistentVolumeClaim (PVC) 是對 PV 的申請 (Claim)。PVC 通常由普通用戶創建和維護。需要為 Pod 分配存儲資源時,用戶可以創建一個 PVC,指明存儲資源的容量大小和訪問模式(比如只讀)等信息,Kubernetes 會查找並提供滿足條件的 PV。
有了 PersistentVolumeClaim,用戶只需要告訴 Kubernetes 需要什么樣的存儲資源,而不必關心真正的空間從哪里分配,如何訪問等底層細節信息。這些 Storage Provider 的底層信息交給管理員來處理,只有管理員才應該關心創建 PersistentVolume 的細節信息。
Kubernetes 支持多種類型的 PersistentVolume,比如 AWS EBS、Ceph、NFS 等
[root@k8s_master ~]# cat nfs-pv1.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mypv1
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
# storageClassName: nfs #不支持
nfs:
path: /nfsdata/pv1 #指定 PV 在 NFS 服務器上對應的目錄。
server: 192.168.138.130
#下面的也正確
#apiVersion: v1
#kind: PersistentVolume
#metadata:
# name: nfs
#spec:
# capacity:
# storage: 1Mi
# accessModes:
# - ReadWriteMany
# nfs:
# server: 192.168.138.130
# path: "/nfsdata/pv1"
capacity 指定 PV 的容量為 1G。
accessModes 指定訪問模式為 ReadWriteOnce,支持的訪問模式有:
ReadWriteOnce – PV 能以 read-write 模式 mount 到單個節點。
ReadOnlyMany – PV 能以 read-only 模式 mount 到多個節點。
ReadWriteMany – PV 能以 read-write 模式 mount 到多個節點。
persistentVolumeReclaimPolicy 指定當 PV 的回收策略為 Recycle,支持的策略有:
Retain – 需要管理員手工回收。
Recycle – 清除 PV 中的數據,效果相當於執行 rm -rf /thevolume/*。
Delete – 刪除 Storage Provider 上的對應存儲資源,例如 AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume 等。
storageClassName 指定 PV 的 class 為 nfs。相當於為 PV 設置了一個分類,PVC 可以指定 class 申請相應 class 的 PV,這個版本不支持;
[root@k8s_master ~]# kubectl apply -f nfs-pv1.yml
error: error validating "nfs-pv1.yml": error validating data: found invalid field storageClassName for v1.PersistentVolumeSpec; if you choose to ignore these errors, turn validation off with --validate=false
[root@k8s_master ~]# kubectl apply -f nfs-pv1.yml
persistentvolume "mypv1" configured
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Recycle Available 3m
nfs 1Mi RWX Retain Available 5m
[root@k8s_master ~]# kubectl apply -f nfs-pvc1.yml
persistentvolumeclaim "mypvc1" created
[root@k8s_master ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
mypvc1 Bound mypv1 1Gi RWO 17s
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Recycle Bound default/mypvc1 10m
nfs 1Mi RWX Retain Available 11m
[root@k8s_master ~]# cat nfs-pvc1.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mypvc1
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
[root@k8s_master ~]# cat pod1.yml
kind: Pod
apiVersion: v1
metadata:
name: mypod1
spec:
containers:
- name: mypod1
image: busybox
args:
- /bin/sh
- -c
- sleep 30000
volumeMounts:
- mountPath: "/mydata"
name: mydata
volumes:
- name: mydata
persistentVolumeClaim:
claimName: mypvc1
[root@k8s_master ~]# kubectl apply -f pod1.yml
pod "mypod1" created
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mypod1 1/1 Running 0 10s 172.8.18.4 192.168.138.161
[root@k8s_master ~]# kubectl exec mypod1 touch /mydata/houpj
[root@k8s_master ~]# ls /nfsdata/pv1/
Houpj
回收
[root@k8s_master ~]# kubectl delete pvc mypvc1
persistentvolumeclaim "mypvc1" deleted
[root@k8s_master ~]# kubectl get pod -o wide
recycler-for-mypv1 0/1 ContainerCreating 0 11s <none> 192.168.138.161
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Recycle Released default/mypvc1 25m
nfs 1Mi RWX Retain Available 27m
[root@k8s_master ~]# ls /nfsdata/pv1/
Houpj
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Recycle Failed default/mypvc1 30m
nfs 1Mi RWX Retain Available 32m
[root@k8s_master ~]# kubectl apply -f nfs-pv1.yml
persistentvolume "mypv1" configured
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Retain Available 49s
nfs 1Mi RWX Retain Available 35m
[root@k8s_master ~]# cat nfs-pv1.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mypv1
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
# storageClassName: nfs #不支持
nfs:
path: /nfsdata/pv1
server: 192.168.138.130
#下面的也正確
#apiVersion: v1
#kind: PersistentVolume
#metadata:
# name: nfs
#spec:
# capacity:
# storage: 1Mi
# accessModes:
# - ReadWriteMany
# nfs:
# server: 192.168.138.130
# path: "/nfsdata/pv1"
[root@k8s_master ~]# cat nfs-pvc1.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mypvc1
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
[root@k8s_master ~]# ls /nfsdata/pv1/
houpj
[root@k8s_master ~]# cd /nfsdata/pv1/
[root@k8s_master pv1]# ls
houpj
[root@k8s_master ~]# kubectl apply -f nfs-pvc1.yml
persistentvolumeclaim "mypvc1" created
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mypod1 1/1 Running 0 23m 172.8.18.4 192.168.138.161
[root@k8s_master ~]# kubectl exec mypod1 touch /mydata/hello
[root@k8s_master ~]# ls /nfsdata/pv1/
hello
[root@k8s_master ~]# kubectl delete pvc mypvc1
persistentvolumeclaim "mypvc1" deleted
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Retain Released default/mypvc1 6m
nfs 1Mi RWX Retain Available 41m
[root@k8s_master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mypod1 1/1 Running 0 25m 172.8.18.4 192.168.138.161
[root@k8s_master ~]# ls /nfsdata/pv1
Hello
雖然 mypv1 中的數據得到了保留,但其 PV 狀態會一直處於 Released,不能被其他 PVC 申請。為了重新使用存儲資源,可以刪除並重新創建 mypv1。刪除操作只是刪除了 PV 對象,存儲空間中的數據並不會被刪除。
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Retain Released default/mypvc1 12m
nfs 1Mi RWX Retain Available 47m
[root@k8s_master ~]# kubectl delete pv mypv1
persistentvolume "mypv1" deleted
[root@k8s_master ~]# kubectl apply -f nfs-pv1.yml
persistentvolume "mypv1" created
[root@k8s_master ~]# kubectl get pv
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
mypv1 1Gi RWO Retain Available 5s
nfs 1Mi RWX Retain Available 48m
新建的 mypv1 狀態為 Available,已經可以被 PVC 申請。
PV 還支持 Delete 的回收策略,會刪除 PV 在 Storage Provider 上對應存儲空間。NFS 的 PV 不支持 Delete,支持 Delete 的 Provider 有 AWS EBS、GCE PD、Azure Disk、OpenStack Cinder Volume 等
Pv 動態供給沒有條件實驗:https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner
問題:
一般實際中容器化mysql是采用deployment還是采用statefulset?另外容器化數據庫會采用哪種類型的存儲多一點?
如果是單個mysql實例,用deployment就可以了。如果是集群,可以用statefulset。statefulset和deploymenty都能用volume,區別主要在於statefulset中的pod都有明確的命名規則,以及啟動順序。存儲的類型不一定,開源ceph較多。
[root@k8s_master ~]# cat mysql-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
# storageClassName: nfs #不支持
nfs:
path: /nfsdata/mysql-pv
server: 192.168.138.130
[root@k8s_master ~]# cat mysql-pvc.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
[root@k8s_master ~]# kubectl apply -f mysql-pv.yml
persistentvolume "mysql-pv" created
[root@k8s_master ~]# kubectl apply -f mysql-pvc.yml
persistentvolumeclaim "mysql-pvc" created
[root@k8s_master ~]# kubectl get pv,pvc
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE
pv/mypv1 1Gi RWO Retain Bound default/mysql-pvc 21m
pv/mysql-pv 1Gi RWO Retain Available 45s
pv/nfs 1Mi RWX Retain Available 1h
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
pvc/mysql-pvc Bound mypv1 1Gi RWO 24s
http://blog.51cto.com/goome/2134294?source=dra 格式很重要
[root@master ~]# cat mysql-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs
nfs:
path: /nfsdata/mysql-pv
server: 192.168.138.166
[root@master ~]# cat mysql-pvc.yml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: nfs
[root@master ~]# cat mysql2.yml
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: mysql-pv
[root@master ~]# kubectl apply -f mysql2.yml
service "mysql" created
deployment "mysql" created
一直報錯
[root@master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mysql-774cffbbcd-vw96h 0/1 ContainerCreating 0 11m <none> minion1
[root@master ~]# kubectl describe pod mysql-6b5cb74bfc-rbczg
Name: mysql-6b5cb74bfc-rbczg
Namespace: default
Node: minion1/192.168.138.130
Start Time: Tue, 18 Sep 2018 19:55:10 +0800
Labels: app=mysql
mount: wrong fs type, bad option, bad superblock on 192.168.138.166:/nfsdata/k8s/mysqlpv1,
missing codepage or helper program, or other error
(for several filesystems (e.g. nfs, cifs) you might
need a /sbin/mount.<type> helper program)
缺少nfs客戶端
[root@minion1 ~]# yum install nfs-utils
[root@master ~]# kubectl delete -f mysql-svc.yml
service "mysql" deleted
deployment "mysql" deleted
[root@master ~]# kubectl apply -f mysql-svc.yml
service "mysql" created
deployment "mysql" created
[root@master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mysql-6b5cb74bfc-mpxs5 1/1 Running 0 8s 10.244.1.3 minion1
[root@minion1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5a2a68004c33 mysql@sha256:4c44f46efaff3ebe7cdc7b35a616c77aa003dc5de4b26c80d0ccae1f9db4a372 "docker-entrypoint..." About an hour ago Up About an hour k8s_mysql_mysql-6b5cb74bfc-mpxs5_default_73394f6f-bb3a-11e8-8d88-000c29f19fb4_0
[root@minion1 ~]# docker inspect 5a2a68004c33
{
"Type": "bind",
"Source": "/var/lib/kubelet/pods/73394f6f-bb3a-11e8-8d88-000c29f19fb4/volumes/kubernetes.io~nfs/mysqlpv1",
"Destination": "/var/lib/mysql",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
[root@minion1 ~]# docker exec -it 5a2a /bin/bash
root@mysql-6b5cb74bfc-mpxs5:/# mysql -uroot -ppassword
mysql> create database t1;
Query OK, 1 row affected (0.03 sec)
mysql> use t1;create table t1(i int);
Database changed
Query OK, 0 rows affected (0.06 sec)
mysql> use t1;insert into t1 values (1),(3),(3);
Database changed
Query OK, 3 rows affected (0.03 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> use t1;select * from t1;
Database changed
+------+
| i |
+------+
| 1 |
| 3 |
| 3 |
+------+
3 rows in set (0.00 sec)
[root@master ~]# ls /nfsdata/k8s/mysqlpv1/
auto.cnf ibdata1 ib_logfile0 ib_logfile1 mysql performance_schema t1
模擬故障
[root@minion1 ~]# init 0
很長時間仍然顯示正常,節點不切換;
[root@master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
mysql-6b5cb74bfc-mpxs5 1/1 Running 0 1h
https://www.sohu.com/a/127037247_515888
[root@master ~]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready master 16h v1.9.0
minion1 NotReady <none> 16h v1.9.0
[root@master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
mysql-6b5cb74bfc-mpxs5 1/1 Running 0 14h
[root@master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
mysql-6b5cb74bfc-mpxs5 1/1 Running 0 14h 10.244.1.3 minion1
啟動宕機節點任然應用運行
[root@minion1 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6f27926b39f3 mysql@sha256:4c44f46efaff3ebe7cdc7b35a616c77aa003dc5de4b26c80d0ccae1f9db4a372 "docker-entrypoint..." 38 seconds ago Up 37 seconds k8s_mysql_mysql-6b5cb74bfc-mpxs5_default_73394f6f-bb3a-11e8-8d88-000c29f19fb4_1
數據仍然在
4.13 Secret
應用啟動過程中可能需要一些敏感信息,比如訪問數據庫的用戶名密碼或者秘鑰。將這些信息直接保存在容器鏡像中顯然不妥,Kubernetes 提供的解決方案是 Secret。
Secret 會以密文的方式存儲數據,避免了直接在配置文件中保存敏感信息。Secret 會以 Volume 的形式被 mount 到 Pod,容器可通過文件的方式使用 Secret 中的敏感數據;此外,容器也可以環境變量的方式使用這些數據。
[root@master ~]# echo -n admin|base64
YWRtaW4=
[root@master ~]# echo -n 123456 |base64
MTIzNDU2
[root@master ~]# kubectl apply -f secret.yml
secret "mysecret" created
[root@master ~]# cat secret.yml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
data:
username: YWRtaW4=
password: MTIzNDU2
[root@master ~]# kubectl get secret
NAME TYPE DATA AGE
default-token-ps7cs kubernetes.io/service-account-token 3 19h
mysecret Opaque 2 1h
[root@master ~]# kubectl get secret mysecret
NAME TYPE DATA AGE
mysecret Opaque 2 1h
[root@master ~]# kubectl describe secret mysecret
Name: mysecret
Namespace: default
Labels: <none>
Annotations:
Type: Opaque
Data
====
password: 6 bytes
username: 5 bytes
[root@master ~]# kubectl edit secret mysecret
apiVersion: v1
data:
password: MTIzNDU2
username: YWRtaW4=
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","data":{"password":"MTIzNDU2","username":"YWRtaW4="},"kind":"Secret","metadata":{"annotations":{},"name":"mysecret","namespace":"default"}}
creationTimestamp: 2018-09-19T03:53:12Z
name: mysecret
namespace: default
resourceVersion: "70131"
selfLink: /api/v1/namespaces/default/secrets/mysecret
uid: 87a7dda2-bbbf-11e8-8d88-000c29f19fb4
type: Opaque
[root@master ~]# echo -n YWRtaW4=| base64 --decode
admin[root@master ~]#
[root@master ~]# echo -n MTIzNDU2|base64 --decode
123456[root@master ~]#
[root@master ~]# cat mypod.yml
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mypod
image: busybox
args:
- /bin/sh
- -c
- sleep 10; touch /tmp/healthy; sleep 30000
volumeMounts:
- name: foo
mountPath: "/etc/foo"
readOnly: true
volumes:
- name: foo
secret:
secretName: mysecret
[root@master ~]# kubectl apply -f mypod.yml
pod "mypod" created
[root@master ~]# kubectl exec -it mypod sh
/ # ls /etc/foo
password username
/ # cat /etc/foo/username
admin/ #
/ # cat /etc/foo/password
123456/ # [root@master ~]#
[root@master ~]# cat secret.yml
apiVersion: v1
kind: Secret
metadata:
name: mysecret
data:
username: YWRtaW4=
password: YWJjZGVm
[root@master ~]# kubectl apply -f secret.yml
secret "mysecret" configured
[root@master ~]# kubectl exec -it mypod sh
/ # ls /etc/foo
password username
/ # cat /etc/foo/username
admin/ #
/ # cat /etc/foo/password
abcdef/ # [root@master ~]#
[root@master ~]# kubectl apply -f mypod.yml
pod "mypod" created
[root@master ~]# cat mypod.yml
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mypod
image: busybox
args:
- /bin/sh
- -c
- sleep 10; touch /tmp/healthy; sleep 30000
volumeMounts:
- name: foo
mountPath: "/etc/foo"
readOnly: true
volumes:
- name: foo
secret:
secretName: mysecret
items:
- key: username
path: my-group/my-username
- key: password
path: my-group/my-password
[root@master ~]# kubectl exec -it mypod sh
/ #
/ # cd /etc/foo
/etc/foo # ls
my-group
/etc/foo # cd my-group/
/etc/foo/my-group # ls
my-password my-username
/etc/foo/my-group # cat my-password
abcdef/etc/foo/my-group #
/etc/foo/my-group # cat my-username
admin/etc/foo/my-group #
[root@master ~]# kubectl apply -f mypod.yml
pod "mypod" created
[root@master ~]# cat mypod.yml
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mypod
image: busybox
args:
- /bin/sh
- -c
- sleep 10; touch /tmp/healthy; sleep 30000
env:
- name: SECRET_USERNAME
valueFrom:
secretKeyRef:
name: mysecret
key: username
- name: SECRET_PASSWORD
valueFrom:
secretKeyRef:
name: mysecret
key: password
[root@master ~]# kubectl exec -it mypod sh
/ # echo $SECRET_USERNAME
admin
/ # echo $SECRET_PASSWORD
Abcdef
通過環境變量 SECRET_USERNAME 和 SECRET_PASSWORD 成功讀取到 Secret 的數據。
需要注意的是,環境變量讀取 Secret 很方便,但無法支撐 Secret 動態更新。
4.13.1 ConfigMap
Secret 可以為 Pod 提供密碼、Token、私鑰等敏感數據;對於一些非敏感數據,比如應用的配置信息,則可以用 ConfigMap。
ConfigMap 的創建和使用方式與 Secret 非常類似,主要的不同是數據以明文的形式存放。
[root@master ~]# kubectl apply -fconfigmap.yml
configmap "myconfigmap" created
[root@master ~]# kubectl apply -f mypod2.yml
pod "mypod" created
[root@master ~]# kubectl get configmap myconfigmap
NAME DATA AGE
myconfigmap 2 3m
[root@master ~]# kubectl describe configmap myconfigmap
Name: myconfigmap
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","data":{"config1":"xxx","config2":"yyy"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"myconfigmap","namespace":"default"}...
Data
====
config1:
----
xxx
config2:
----
yyy
Events: <none>
[root@master ~]# cat configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: myconfigmap
data:
config1: xxx
config2: yyy
[root@master ~]# kubectl apply -f configmap2.yml
configmap "myconfigmap" created
[root@master ~]# cat configmap2.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: myconfigmap
data:
logging.conf: |
class: logging.handlers.RotatingFileHandler
formatter: precise
level: INFO
filename: %hostname-%timestamp.log
[root@master ~]# kubectl get configmap myconfigmap
NAME DATA AGE
myconfigmap 1 30s
[root@master ~]# kubectl describe configmap myconfigmap
Name: myconfigmap
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v1","data":{"logging.conf":"class: logging.handlers.RotatingFileHandler\nformatter: precise\nlevel: INFO\nfilename: %hostname-%timestamp...
Data
====
logging.conf:
----
class: logging.handlers.RotatingFileHandler
formatter: precise
level: INFO
filename: %hostname-%timestamp.log
Events: <none>
[root@master ~]# cat mypod4.yml
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mypod
image: busybox
args:
- /bin/sh
- -c
- sleep 10; touch /tmp/healthy; sleep 30000
volumeMounts:
- name: foo
mountPath: "/etc"
volumes:
- name: foo
configMap:
name: myconfigmap
items:
- key: logging.conf
path: myapp/logging.conf
[root@master ~]# kubectl apply -f mypod4.yml
pod "mypod" created
[root@master ~]# kubectl get pods
NAME READY STATUS RESTARTS AGE
mypod 1/1 Running 0 15s
mysql-6b5cb74bfc-mpxs5 1/1 Running 1 18h
[root@master ~]# kubectl exec -it mypod sh
/ # cat /etc/myapp/logging.conf
class: logging.handlers.RotatingFileHandler
formatter: precise
level: INFO
filename: %hostname-%timestamp.log
/ #
第5章 Helm
5.1 概念
每個成功的軟件平台都有一個優秀的打包系統,比如 Debian、Ubuntu 的 apt,Redhat、Centos 的 yum。而 Helm 則是 Kubernetes 上的包管理器。
本章我們將討論為什么需要 Helm,它的架構和組件,以及如何使用 Helm。
Why Helm
Helm 到底解決了什么問題?為什么 Kubernetes 需要 Helm?
答案是:Kubernetes 能夠很好地組織和編排容器,但它缺少一個更高層次的應用打包工具,而 Helm 就是來干這件事的。
先來看個例子。
比如對於一個 MySQL 服務, Kubernetes 需要部署下面這些對象:
我們可以將上面這些配置保存到對象各自的文件中,或者集中寫進一個配置文件,然后通過 kubectl apply -f 部署。
到目前為止,Kubernetes 對服務的部署支持得都挺好,如果應用只由一個或幾個這樣的服務組成,上面的部署方式完全足夠了。
但是,如果我們開發的是微服務架構的應用,組成應用的服務可能多達十個甚至幾十上百個,這種組織和管理應用的方式就不好使了:
很難管理、編輯和維護如此多的服務。每個服務都有若干配置,缺乏一個更高層次的工具將這些配置組織起來。
不容易將這些服務作為一個整體統一發布。部署人員需要首先理解應用都包含哪些服務,然后按照邏輯順序依次執行 kubectl apply。即缺少一種工具來定義應用與服務,以及服務與服務之間的依賴關系。
不能高效地共享和重用服務。比如兩個應用都要用到 MySQL 服務,但配置的參數不一樣,這兩個應用只能分別拷貝一套標准的 MySQL 配置文件,修改后通過 kubectl apply 部署。也就是說不支持參數化配置和多環境部署。
不支持應用級別的版本管理。雖然可以通過 kubectl rollout undo 進行回滾,但這只能針對單個 Deployment,不支持整個應用的回滾。
不支持對部署的應用狀態進行驗證。比如是否能通過預定義的賬號訪問 MySQL。雖然 Kubernetes 有健康檢查,但那是針對單個容器,我們需要應用(服務)級別的健康檢查。
Helm 能夠解決上面這些問題,Helm 幫助 Kubernetes 成為微服務架構應用理想的部署平台。Helm 有兩個重要的概念:chart 和 release。
chart 是創建一個應用的信息集合,包括各種 Kubernetes 對象的配置模板、參數定義、依賴關系、文檔說明等。chart 是應用部署的自包含邏輯單元。可以將 chart 想象成 apt、yum 中的軟件安裝包。
release 是 chart 的運行實例,代表了一個正在運行的應用。當 chart 被安裝到 Kubernetes 集群,就生成一個 release。chart 能夠多次安裝到同一個集群,每次安裝都是一個 release。
Helm 是包管理工具,這里的包就是指的 chart。Helm 能夠:
從零創建新 chart。
與存儲 chart 的倉庫交互,拉取、保存和更新 chart。
在 Kubernetes 集群中安裝和卸載 release。
更新、回滾和測試 release。
Helm 包含兩個組件:Helm 客戶端 和 Tiller 服務器。
Helm 客戶端是終端用戶使用的命令行工具,用戶可以:
在本地開發 chart。
管理 chart 倉庫。
與 Tiller 服務器交互。
在遠程 Kubernetes 集群上安裝 chart。
查看 release 信息。
升級或卸載已有的 release。
Tiller 服務器運行在 Kubernetes 集群中,它會處理 Helm 客戶端的請求,與 Kubernetes API Server 交互。Tiller 服務器負責:
監聽來自 Helm 客戶端的請求。
通過 chart 構建 release。
在 Kubernetes 中安裝 chart,並跟蹤 release 的狀態。
通過 API Server 升級或卸載已有的 release。
簡單的講:Helm 客戶端負責管理 chart;Tiller 服務器負責管理 release。
5.2 部署
https://yq.aliyun.com/articles/159601 tiller 安裝
[root@master ~]# tar xf helm-v2.11.0-linux-amd64.tar.gz -C /tmp/helm-installer/helm/
[root@master linux-amd64]# ls
helm LICENSE README.md tiller
[root@master linux-amd64]# cp helm /usr/local/bin/
[root@master ~]# helm version
Client: &version.Version{SemVer:"v2.11.0", GitCommit:"2e55dbe1fdb5fdb96b75ff144a339489417b146b", GitTreeState:"clean"}
Error: could not find tiller
helm init --upgrade -i registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.5.1 --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
[root@master ~]# helm version
Client: &version.Version{SemVer:"v2.11.0", GitCommit:"2e55dbe1fdb5fdb96b75ff144a339489417b146b", GitTreeState:"clean"}
Server: &version.Version{SemVer:"v2.5.1", GitCommit:"7cf31e8d9a026287041bae077b09165be247ae66", GitTreeState:"clean"}
[root@master ~]# kubectl get --namespace=kube-system svc tiller-deploy
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
tiller-deploy ClusterIP 10.108.109.87 <none> 44134/TCP 7m
[root@master ~]# kubectl get --namespace=kube-system deployment tiller-deploy
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
tiller-deploy 1 1 1 1 8m
[root@master ~]# kubectl get --namespace=kube-system pod
tiller-deploy-86558fdd5b-4bnd9 1/1 Running 0 11m
Helm 安裝成功后,可執行 helm search 查看當前可安裝的 chart。
[root@master ~]# helm search|head -10
NAME CHART VERSION APP VERSION DESCRIPTION
stable/acs-engine-autoscaler 2.1.3 2.1.1 Scales worker nodes within agent pools
stable/aerospike 0.1.7 v3.14.1.2 A Helm chart for Aerospike in Kubernetes
stable/anchore-engine 0.1.3 0.1.6 Anchore container analysis and policy evaluation engine s...
stable/artifactory 7.0.3 5.8.4 Universal Repository Manager supporting all major packagi...
stable/artifactory-ha 0.1.0 5.8.4 Universal Repository Manager supporting all major packagi...
stable/aws-cluster-autoscaler 0.3.2 Scales worker nodes within autoscaling groups.
stable/bitcoind 0.1.0 0.15.1 Bitcoin is an innovative payment network and a new kind o...
stable/buildkite 0.2.1 3 Agent for Buildkite
stable/centrifugo 2.0.0 1.7.3 Centrifugo is a real-time messaging server.
前面說過,Helm 可以像 apt 和 yum 管理軟件包一樣管理 chart。apt 和 yum 的軟件包存放在倉庫中,同樣的,Helm 也有倉庫。
[root@master ~]# helm repo list
NAME URL
stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
local http://127.0.0.1:8879/charts
前面說過,Helm 可以像 apt 和 yum 管理軟件包一樣管理 chart。apt 和 yum 的軟件包存放在倉庫中,同樣的,Helm 也有倉庫。
Helm 安裝時已經默認配置好了兩個倉庫:stable 和 local。stable 是官方倉庫,local 是用戶存放自己開發的 chart 的本地倉庫。
helm search 會顯示 chart 位於哪個倉庫,比如 local/cool-chart 和 stable/acs-engine-autoscaler。
用戶可以通過 helm repo add 添加更多的倉庫,比如企業的私有倉庫,倉庫的管理和維護方法請參考官網文檔 https://docs.helm.sh
與 apt 和 yum 一樣,helm 也支持關鍵字搜索:
[root@master ~]# helm search mysql
NAME CHART VERSION APP VERSION DESCRIPTION
stable/mysql 0.3.5 Fast, reliable, scalable, and easy to use open-source rel...
stable/percona 0.3.0 free, fully compatible, enhanced, open source drop-in rep...
stable/percona-xtradb-cluster 0.0.2 5.7.19 free, fully compatible, enhanced, open source drop-in rep...
stable/gcloud-sqlproxy 0.2.3 Google Cloud SQL Proxy
stable/mariadb 2.1.6 10.1.31 Fast, reliable, scalable, and easy to use open-source rel..
包括 DESCRIPTION 在內的所有信息,只要跟關鍵字匹配,都會顯示在結果列表中。
安裝 chart 也很簡單,執行如下命令可以安裝 MySQL。
helm install stable/mysql
沒有權限報錯
[root@master ~]# helm install stable/mysql
Error: no available release name found
開啟權限
[root@master ~]# kubectl create serviceaccount --namespace kube-system tiller
serviceaccount "tiller" created
[root@master ~]# kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
clusterrolebinding "tiller-cluster-rule" created
[root@master ~]# kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'
deployment "tiller-deploy" patched
[root@master ~]# helm install stable/mysql
NAME: wrapping-hog
LAST DEPLOYED: Fri Sep 21 00:29:02 2018
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Secret
NAME TYPE DATA AGE
wrapping-hog-mysql Opaque 2 1d
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS AGE
wrapping-hog-mysql Pending 1d
==> v1/Service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
wrapping-hog-mysql 10.104.115.37 <none> 3306/TCP 1d
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
wrapping-hog-mysql 1 1 1 0 1d
NOTES:
MySQL can be accessed via port 3306 on the following DNS name from within your cluster:
wrapping-hog-mysql.default.svc.cluster.local
To get your root password run:
MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace default wrapping-hog-mysql -o jsonpath="{.data.mysql-root-password}" | base64 --decode; echo)
To connect to your database:
1. Run an Ubuntu pod that you can use as a client:
kubectl run -i --tty ubuntu --image=ubuntu:16.04 --restart=Never -- bash -il
2. Install the mysql client:
$ apt-get update && apt-get install mysql-client -y
3. Connect using the mysql cli, then provide your password:
$ mysql -h wrapping-hog-mysql -p
To connect to your database directly from outside the K8s cluster:
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
# Execute the following commands to route the connection:
export POD_NAME=$(kubectl get pods --namespace default -l "app=wrapping-hog-mysql" -o jsonpath="{.items[0].metadata.name}")
kubectl port-forward $POD_NAME 3306:3306
mysql -h ${MYSQL_HOST} -P${MYSQL_PORT} -u root -p${MYSQL_ROOT_PASSWORD}
綠色部分
chart 本次部署的描述信息:
NAME 是 release 的名字,因為我們沒用 -n 參數指定,Helm 隨機生成了一個,這里是 wrapping-hog。
NAMESPACE 是 release 部署的 namespace,默認是 default,也可以通過 --namespace 指定。
STATUS 為 DEPLOYED,表示已經將 chart 部署到集群
當前 release 包含的資源:Service、Deployment、Secret 和 PersistentVolumeClaim,其名字都是 wrapping-hog-mysql,命名的格式為 ReleasName-ChartName。
NOTES 部分顯示的是 release 的使用方法。比如如何訪問 Service,如何獲取數據庫密碼,以及如何連接數據庫等。
[root@master ~]# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 23h
mysql ClusterIP 10.108.20.158 <none> 3306/TCP 21h
wrapping-hog-mysql ClusterIP 10.104.115.37 <none> 3306/TCP 8m
[root@master ~]# kubectl get deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
mysql 1 1 1 1 21h
wrapping-hog-mysql 1 1 1 0 8m
[root@master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
mypod 1/1 Running 0 3h
mysql-6b5cb74bfc-mpxs5 1/1 Running 1 21h
wrapping-hog-mysql-697f95c9dd-b2t2s 0/1 Init:0/1 0 9m
[root@master ~]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mysqlpvc1 Bound mysqlpv1 1Gi RWO nfs1 21h
wrapping-hog-mysql Pending
[root@master ~]# helm list
NAME REVISION UPDATED STATUS CHART NAMESPACE
wrapping-hog 1 Fri Sep 21 00:29:02 2018 DEPLOYED mysql-0.3.5 default
[root@master ~]# helm delete wrapping-hog
release "wrapping-hog" deleted 9m
chart 是 Helm 的應用打包格式。chart 由一系列文件組成,這些文件描述了 Kubernetes 部署應用時所需要的資源,比如 Service、Deployment、PersistentVolumeClaim、Secret、ConfigMap 等。
單個的 chart 可以非常簡單,只用於部署一個服務,比如 Memcached;chart 也可以很復雜,部署整個應用,比如包含 HTTP Servers、 Database、消息中間件、cache 等。
chart 將這些文件放置在預定義的目錄結構中,通常整個 chart 被打成 tar 包,而且標注上版本信息,便於 Helm 部署。
[root@master ~]# ls .helm/cache/archive/
mysql-0.3.5.tgz
[root@master ~]# tree mysql
mysql
├── Chart.yaml
├── README.md
├── templates
│ ├── configmap.yaml
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── NOTES.txt
│ ├── pvc.yaml
│ ├── secrets.yaml
│ └── svc.yaml
└── values.yaml
1 directory, 10 files
Chart.yaml
YAML 文件,描述 chart 的概要信息。
name 和 version 是必填項,其他都是可選。
README.md
Markdown 格式的 README 文件,相當於 chart 的使用文檔,此文件為可選。
LICENSE
文本文件,描述 chart 的許可信息,此文件為可選。
requirements.yaml
chart 可能依賴其他的 chart,這些依賴關系可通過 requirements.yaml 指定,比如:
在安裝過程中,依賴的 chart 也會被一起安裝。
values.yaml
chart 支持在安裝的時根據參數進行定制化配置,而 values.yaml 則提供了這些配置參數的默認值。
templates 目錄
各類 Kubernetes 資源的配置模板都放置在這里。Helm 會將 values.yaml 中的參數值注入到模板中生成標准的 YAML 配置文件。
模板是 chart 最重要的部分,也是 Helm 最強大的地方。模板增加了應用部署的靈活性,能夠適用不同的環境,我們后面會詳細討論。
templates/NOTES.txt
chart 的簡易使用文檔,chart 安裝成功后會顯示此文檔內容。
與模板一樣,可以在 NOTE.txt 中插入配置參數,Helm 會動態注入參數值。
5.3 Chart模板
[root@master templates]# cat secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ template "mysql.fullname" . }}
labels:
app: {{ template "mysql.fullname" . }}
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
release: "{{ .Release.Name }}"
heritage: "{{ .Release.Service }}"
type: Opaque
data:
{{ if .Values.mysqlRootPassword }}
mysql-root-password: {{ .Values.mysqlRootPassword | b64enc | quote }}
{{ else }}
mysql-root-password: {{ randAlphaNum 10 | b64enc | quote }}
{{ end }}
{{ if .Values.mysqlPassword }}
mysql-password: {{ .Values.mysqlPassword | b64enc | quote }}
{{ else }}
mysql-password: {{ randAlphaNum 10 | b64enc | quote }}
{{ end }}
從結構看,文件的內容非常像Secret 配置,只是大部分屬性值變成了{{ xxx }}。這些 {{ xxx }}實際上是模板的語法。Helm 采用了Go語言的模板來編寫chart。Go模板非常強大,支持變量、對象、函數、流控制等功能。下面我們通過解析templates/secrets.yaml快速學習模板。
{{ template "mysql.fullname" . }} 定義 Secret 的 name。
關鍵字 template 的作用是引用一個子模板 mysql.fullname。這個子模板是在 templates/_helpers.tpl 文件中定義的
[root@master templates]# cat _helpers.tpl
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "mysql.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "mysql.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
這個定義還是很復雜的,因為它用到了模板語言中的對象、函數、流控制等概念。現在看不懂沒關系,這里我們學習的重點是:如果存在一些信息多個模板都會用到,則可在 templates/_helpers.tpl 中將其定義為子模板,然后通過 templates 函數引用。
這里 mysql.fullname 是由 release 與 chart 二者名字拼接組成。
根據 chart 的最佳實踐,所有資源的名稱都應該保持一致,對於我們這個 chart,無論 Secret 還是 Deployment、PersistentVolumeClaim、Service,它們的名字都是子模板 mysql.fullname 的值。
Chart 和 Release 是 Helm 預定義的對象,每個對象都有自己的屬性,可以在模板中使用。如果使用下面命令安裝 chart:
helm install stable/mysql -n my
那么:
{{ .Chart.Name }} 的值為 mysql。
{{ .Chart.Version }} 的值為 0.3.0。
{{ .Release.Name }} 的值為 my。
{{ .Release.Service }} 始終取值為 Tiller。
{{ template "mysql.fullname" . }} 計算結果為 my-mysql。
{{ if .Values.mysqlRootPassword }}
mysql-root-password: {{ .Values.mysqlRootPassword | b64enc | quote }}
{{ else }}
mysql-root-password: {{ randAlphaNum 10 | b64enc | quote }}
{{ end }}
{{ if .Values.mysqlPassword }}
mysql-password: {{ .Values.mysqlPassword | b64enc | quote }}
{{ else }}
mysql-password: {{ randAlphaNum 10 | b64enc | quote }}
{{ end }}
這里指定 mysql-root-password 的值,不過使用了 if-else 的流控制,其邏輯為:
如果 .Values.mysqlRootPassword 有值,則對其進行 base64 編碼;否則隨機生成一個 10 位的字符串並編碼。
Values 也是預定義的對象,代表的是 values.yaml 文件。而 .Values.mysqlRootPassword 則是 values.yaml 中定義的 mysqlRootPassword 參數:
[root@master mysql]# cat values.yaml |head -20
## mysql image version
## ref: https://hub.docker.com/r/library/mysql/tags/
##
image: "mysql"
imageTag: "5.7.14"
## Specify password for root user
##
## Default: random 10 character string
# mysqlRootPassword: testing
## Create a database user
##
# mysqlUser:
# mysqlPassword:
## Allow unauthenticated access, uncomment to enable
##
# mysqlAllowEmptyPassword: true
因為 mysqlRootPassword 被注釋掉了,沒有賦值,所以邏輯判斷會走 else,即隨機生成密碼。
randAlphaNum、b64enc、quote 都是 Go 模板語言支持的函數,函數之間可以通過管道 | 連接。{{ randAlphaNum 10 | b64enc | quote }} 的作用是首先隨機產生一個長度為 10 的字符串,然后將其 base64 編碼,最后兩邊加上雙引號。
templates/secrets.yaml 這個例子展示了 chart 模板主要的功能,我們最大的收獲應該是:模板將 chart 參數化了,通過 values.yaml 可以靈活定制應用
5.4 實踐
作為准備工作,安裝之前需要先清楚 chart 的使用方法。這些信息通常記錄在 values.yaml 和 README.md 中。除了下載源文件查看,執行 helm inspect values 可能是更方便的方法。
[root@master ~]# helm inspect values stable/mysql
## mysql image version
## ref: https://hub.docker.com/r/library/mysql/tags/
##
image: "mysql"
imageTag: "5.7.14"
輸出的實際上是 values.yaml 的內容。閱讀注釋就可以知道 MySQL chart 支持哪些參數,安裝之前需要做哪些准備。其中有一部分是關於存儲的:
## Persist data to a persistent volume
persistence:
enabled: true
## database data Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
accessMode: ReadWriteOnce
size: 8Gi
chart 定義了一個 PersistentVolumeClaim,申請 8G 的 PersistentVolume。由於我們的實驗環境不支持動態供給,所以得預先創建好相應的 PV,其配置文件 mysql-pv.yml 內容為:
[root@master ~]# cat mysql-pv.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 8Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
# storageClassName: nfs1
nfs:
path: /nfsdata/mysql-pv
server: 192.168.138.166
[root@master ~]# kubectl apply -f mysql-pv.yml
persistentvolume "mysql-pv" created
[root@master ~]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
mysql-pv 8Gi RWO Retain Available 15s
定制化安裝 chart
除了接受 values.yaml 的默認值,我們還可以定制化 chart,比如設置 mysqlRootPassword。
Helm 有兩種方式傳遞配置參數:
指定自己的 values 文件。
通常的做法是首先通過 helm inspect values mysql > myvalues.yaml生成 values 文件,然后設置 mysqlRootPassword,之后執行 helm install --values=myvalues.yaml mysql。
通過 --set 直接傳入參數值,比如
[root@master ~]# helm install stable/mysql --set mysqlRootPassword=abc123 -n my
NAME: my
LAST DEPLOYED: Fri Sep 21 03:23:31 2018
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Secret
NAME TYPE DATA AGE
my-mysql Opaque 2 1d
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS AGE
my-mysql Bound mysql-pv 8Gi RWO 1d
==> v1/Service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-mysql 10.111.229.211 <none> 3306/TCP 1d
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
my-mysql 1 1 1 0 1d
mysqlRootPassword 設置為 abc123。另外,-n 設置 release 為 my,各類資源的名稱即為my-mysql。
通過 helm list 和 helm status 可以查看 chart 的最新狀態。
[root@master ~]# helm status my
LAST DEPLOYED: Fri Sep 21 03:23:31 2018
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/PersistentVolumeClaim
NAME STATUS VOLUME CAPACITY ACCESSMODES STORAGECLASS AGE
my-mysql Bound mysql-pv 8Gi RWO 1d
==> v1/Service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-mysql 10.111.229.211 <none> 3306/TCP 1d
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
my-mysql 1 1 1 0 1d
==> v1/Secret
NAME TYPE DATA AGE
my-mysql Opaque 2 1d
升級和回滾 release
release 發布后可以執行 helm upgrade 對其升級,通過 --values 或 --set應用新的配置。比如將當前的 MySQL 版本升級到 5.7.15:
[root@master ~]# helm upgrade --set imageTag=5.7.15 my stable/mysql
Error: UPGRADE FAILED: transport is closing
[root@master ~]# kubectl get deployment my-mysql -o wide
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
my-mysql 1 1 1 0 11m my-mysql mysql:5.7.14 app=my-mysql
[root@master ~]# helm history my
REVISION UPDATED STATUS CHART DESCRIPTION
1 Fri Sep 21 03:23:31 2018 DEPLOYED mysql-0.3.5 Install complete
[root@master ~]# helm rollback my 1 #回滾
Kubernetes 給我們提供了大量官方 chart,不過要部署微服務應用,還是需要開發自己的 chart,下面就來實踐這個主題。
[root@master ~]# helm create mychart
Creating mychart
[root@master ~]# tree mychart/
mychart/
├── charts
├── Chart.yaml
├── templates
│ ├── deployment.yaml
│ ├── _helpers.tpl
│ ├── ingress.yaml
│ ├── NOTES.txt
│ └── service.yaml
└── values.yaml
2 directories, 7 files
開發時建議大家參考官方chart中的模板、values.yaml、Chart.yaml,里面包含了大量最佳實踐和最常用的函數、流控制
調試 chart
只要是程序就會有 bug,chart 也不例外。Helm 提供了 debug 的工具:helm lint 和 helm install --dry-run --debug。
helm lint 會檢測 chart 的語法,報告錯誤以及給出建議。
[root@master ~]# helm lint mychart
==> Linting mychart
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, no failures
故意改錯
[root@master mychart]# vim values.yaml
# Default values for mychart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy IfNotPresent
[root@master ~]# helm lint mychart
==> Linting mychart
[INFO] Chart.yaml: icon is recommended
[ERROR] values.yaml: unable to parse YAML
error converting YAML to JSON: yaml: line 8: could not find expected ':'
Error: 1 chart(s) linted, 1 chart(s) failed
helm install --dry-run --debug 會模擬安裝 chart,並輸出每個模板生成的YAML內容。
[root@master ~]# helm install --dry-run mychart --debug
[debug] Created tunnel using local port: '35442'
[debug] SERVER: "localhost:35442"
[debug] Original chart version: ""
[debug] CHART PATH: /root/mychart
NAME: eerie-chinchilla
REVISION: 1
RELEASED: Thu Sep 27 19:57:52 2018
CHART: mychart-0.1.0
USER-SUPPLIED VALUES:
{}
COMPUTED VALUES:
image:
pullPolicy: IfNotPresent
repository: nginx
tag: stable
ingress:
annotations: null
enabled: false
hosts:
- chart-example.local
tls: null
replicaCount: 1
resources: {}
service:
externalPort: 80
internalPort: 80
name: nginx
type: ClusterIP
HOOKS:
MANIFEST:
---
# Source: mychart/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: eerie-chinchilla-mychart
labels:
app: mychart
chart: mychart-0.1.0
release: eerie-chinchilla
heritage: Tiller
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app: mychart
release: eerie-chinchilla
---
# Source: mychart/templates/deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: eerie-chinchilla-mychart
labels:
app: mychart
chart: mychart-0.1.0
release: eerie-chinchilla
heritage: Tiller
spec:
replicas: 1
template:
metadata:
labels:
app: mychart
release: eerie-chinchilla
spec:
containers:
- name: mychart
image: "nginx:stable"
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /
port: 80
readinessProbe:
httpGet:
path: /
port: 80
resources:
{}
我們可以檢視這些輸出,判斷是否與預期相符。
同樣,mychart 目錄作為參數傳遞給 helm install --dry-run --debug。
5.5 管理和安裝chart
當我們覺得准備就緒,就可以安裝 chart,Helm 支持四種安裝方法:
安裝倉庫中的 chart,例如:helm install stable/nginx
通過 tar 包安裝,例如:helm install ./nginx-1.2.3.tgz
通過 chart 本地目錄安裝,例如:helm install ./nginx
通過 URL 安裝,例如:helm install https://example.com/charts/nginx-1.2.3.tgz
[root@master ~]# helm install mychart
NAME: yodeling-opossum
LAST DEPLOYED: Thu Sep 27 20:04:31 2018
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
yodeling-opossum-mychart 10.110.28.223 <none> 80/TCP 2s
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
yodeling-opossum-mychart 1 0 0 0 2s
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods --namespace default -l "app=mychart,release=yodeling-opossum" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl port-forward $POD_NAME 8080:80
將 chart 添加到倉庫
chart 通過測試后可以將其添加到倉庫,團隊其他成員就能夠使用。任何 HTTP Server 都可以用作 chart 倉庫,下面演示在 k8s-node1 上搭建倉庫。
a在 minion1上啟動一個 httpd 容器
[root@minion1 ~]# mkdir /var/www
[root@minion1 ~]# docker run -d -p 8080:80 -v /var/www/:/usr/local/apache2/htdocs/ httpd
b通過helm package將mychart打包。
[root@master ~]# helm package mychart
Successfully packaged chart and saved it to: /root/mychart-0.1.0.tgz
c執行 helm repo index生成倉庫的index文件。
[root@master ~]# helm repo index myrepo/ --url http://192.168.138.130:8080/charts
[root@master ~]# ls myrepo/
index.yaml mychart-0.1.0.tgz
Helm 會掃描 myrepo 目錄中的所有 tgz 包並生成 index.yaml。--url指定的是新倉庫的訪問路徑。新生成的 index.yaml 記錄了當前倉庫中所有 chart 的信息:
[root@master myrepo]# cat index.yaml
apiVersion: v1
entries:
mychart:
- apiVersion: v1
created: 2018-09-27T20:14:44.409532592+08:00
description: A Helm chart for Kubernetes
digest: 08092d2be29635ae9a32858e15ef3abb413b768d13d331d06fde87dd07b6fa21
name: mychart
urls:
- http://192.168.138.130:8080/charts/mychart-0.1.0.tgz
version: 0.1.0
generated: 2018-09-27T20:14:44.409049277+08:00
d將 mychart-0.1.0.tgz 和 index.yaml 上傳到minion1的/var/www/charts目錄。
e通過 helm repo add 將新倉庫添加到 Helm。
[root@master myrepo]# helm repo add newrepo http://192.168.138.130:8080/charts
"newrepo" has been added to your repositories
[root@master myrepo]# helm repo list
NAME URL
stable https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts
local http://127.0.0.1:8879/charts
newrepo http://192.168.138.130:8080/charts
[root@master myrepo]# helm search mychart
NAME VERSION DESCRIPTION
local/mychart 0.1.0 A Helm chart for Kubernetes
newrepo/mychart 0.1.0 A Helm chart for Kubernetes
倉庫命名為 newrepo,Helm會從倉庫下載index.yaml。
除了 newrepo/mychart,這里還有一個 local/mychart。這是因為在執行第 2 步打包操作的同時,mychart 也被同步到了 local 的倉庫。
f已經可以直接從新倉庫安裝 mychart了
[root@master myrepo]# helm install newrepo/mychart
NAME: crusty-newt
LAST DEPLOYED: Thu Sep 27 20:36:05 2018
NAMESPACE: default
STATUS: DEPLOYED
RESOURCES:
==> v1/Service
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
crusty-newt-mychart 10.110.231.167 <none> 80/TCP 0s
==> v1beta1/Deployment
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
crusty-newt-mychart 1 0 0 0 0s
如果以后倉庫添加了新的chart,需要用helm repo update更新本地的index。
[root@master myrepo]# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Skip local chart repository
...Successfully got an update from the "newrepo" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
Helm 讓我們能夠像 apt 管理 deb 包那樣安裝、部署、升級和刪除容器化應用。
Helm 由客戶端和 Tiller 服務器組成。客戶端負責管理 chart,服務器負責管理 release。
chart 是 Helm 的應用打包格式,它由一組文件和目錄構成。其中最重要的是模板,模板中定義了 Kubernetes 各類資源的配置信息,Helm 在部署時通過 values.yaml 實例化模板。
Helm 允許用戶開發自己的 chart,並為用戶提供了調試工具。用戶可以搭建自己的 chart 倉庫,在團隊中共享 chart。
Helm 幫助用戶在 Kubernetes 上高效地運行和管理微服務架構應用,Helm 非常重要。
第6章 網絡模型
6.1 概念
Kubernetes 作為編排引擎管理着分布在不同節點上的容器和 Pod。Pod、Service、外部組件之間需要一種可靠的方式找到彼此並進行通信,Kubernetes 網絡則負責提供這個保障。
Kubernetes 網絡模型
Kubernetes 采用的是基於扁平地址空間的網絡模型,集群中的每個 Pod 都有自己的 IP 地址,Pod 之間不需要配置 NAT 就能直接通信。另外,同一個 Pod 中的容器共享 Pod 的 IP,能夠通過 localhost 通信。
這種網絡模型對應用開發者和管理員相當友好,應用可以非常方便地從傳統網絡遷移到 Kubernetes。每個 Pod 可被看作是一個個獨立的系統,而 Pod 中的容器則可被看做同一系統中的不同進程。
Pod 內容器之間的通信
當 Pod 被調度到某個節點,Pod 中的所有容器都在這個節點上運行,這些容器共享相同的本地文件系統、IPC 和網絡命名空間。
不同 Pod 之間不存在端口沖突的問題,因為每個 Pod 都有自己的 IP 地址。當某個容器使用 localhost 時,意味着使用的是容器所屬 Pod 的地址空間。
比如 Pod A 有兩個容器 container-A1 和 container-A2,container-A1 在端口 1234 上監聽,當 container-A2 連接到 localhost:1234,實際上就是在訪問 container-A1。這不會與同一個節點上的 Pod B 沖突,即使 Pod B 中的容器 container-B1 也在監聽 1234 端口。
Pod 之間的通信
Pod 的 IP 是集群可見的,即集群中的任何其他 Pod 和節點都可以通過 IP 直接與 Pod 通信,這種通信不需要借助任何的網絡地址轉換、隧道或代理技術。Pod 內部和外部使用的是同一個 IP,這也意味着標准的命名服務和發現機制,比如 DNS 可以直接使用。
Pod 與 Service 的通信
Pod 間可以直接通過 IP 地址通信,但前提是 Pod 得知道對方的 IP。在 Kubernetes 集群中, Pod 可能會頻繁的銷毀和創建,也就是說 Pod 的 IP 不是固定的。為了解決這個問題,Service 提供了訪問 Pod 的抽象層。無論后端的 Pod 如何變化,Service 都作為穩定的前端對外提供服務。同時,Service 還提供了高可用和負載均衡功能,Service 負責將請求轉發給正確的 Pod。
外部訪問
無論是 Pod 的 IP 還是 Service 的 Cluster IP,它們只能在 Kubernetes 集群中可見,對集群之外的世界,這些 IP 都是私有的。
Kubernetes 提供了兩種方式讓外界能夠與 Pod 通信:
NodePort
Service 通過 Cluster 節點的靜態端口對外提供服務。外部可以通過 <NodeIP>:<NodePort> 訪問 Service。
LoadBalancer
Service 利用 cloud provider 提供的 load balancer 對外提供服務,cloud provider 負責將 load balancer 的流量導向 Service。目前支持的 cloud provider 有 GCP、AWS、Azur 等。
為了保證網絡方案的標准化、擴展性和靈活性,Kubernetes 采用了 Container Networking Interface(CNI)規范。
CNI 是由 CoreOS 提出的容器網絡規范,它使用了插件(Plugin)模型創建容器的網絡棧。
CNI 的優點是支持多種容器 runtime,不僅僅是 Docker。CNI 的插件模型支持不同組織和公司開發的第三方插件,這對運維人員來說很有吸引力,可以靈活選擇適合的網絡方案。
目前已有多種支持 Kubernetes 的網絡方案,比如 Flannel、Calico、Canal、Weave Net 等。因為它們都實現了 CNI 規范,用戶無論選擇哪種方案,得到的網絡模型都一樣,即每個 Pod 都有獨立的 IP,可以直接通信。區別在於不同方案的底層實現不同,有的采用基於 VxLAN 的 Overlay 實現,有的則是 Underlay,性能上有區別。再有就是是否支持 Network Policy。
6.2 Network Policy
Network Policy 是 Kubernetes 的一種資源。Network Policy 通過 Label 選擇 Pod,並指定其他 Pod 或外界如何與這些 Pod 通信。
默認情況下,所有 Pod 是非隔離的,即任何來源的網絡流量都能夠訪問 Pod,沒有任何限制。當為 Pod 定義了 Network Policy,只有 Policy 允許的流量才能訪問 Pod。
不過,不是所有的 Kubernetes 網絡方案都支持 Network Policy。比如 Flannel 就不支持,Calico 是支持的。我們接下來將用 Canal 來演示 Network Policy。Canal 這個開源項目很有意思,它用 Flannel 實現 Kubernetes 集群網絡,同時又用 Calico 實現 Network Policy。
6.3 部署canal
下載好文件鏡像
[root@master ~]# kubectl apply -f rbac.yaml
clusterrole "calico" created
clusterrole "flannel" created
clusterrolebinding "canal-flannel" created
clusterrolebinding "canal-calico" created
[root@master ~]# kubectl apply -f canal.yaml
configmap "canal-config" created
daemonset "canal" created
customresourcedefinition "globalfelixconfigs.crd.projectcalico.org" created
customresourcedefinition "globalbgpconfigs.crd.projectcalico.org" created
customresourcedefinition "ippools.crd.projectcalico.org" created
customresourcedefinition "globalnetworkpolicies.crd.projectcalico.org" created
serviceaccount "canal" created
[root@master ~]#
[root@master ~]#
[root@master ~]#
[root@master ~]# kubectl get --namespace=kube-system daemonset
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
canal 1 1 1 1 1 <none> 3m
kube-proxy 1 1 1 1 1 <none> 24m
[root@master ~]# cat httpd.yml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: httpd
spec:
replicas: 3
template:
metadata:
labels:
run: httpd
spec:
containers:
- name: httpd
image: httpd:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: httpd-svc
spec:
type: NodePort
selector:
run: httpd
ports:
- protocol: TCP
nodePort: 30000
port: 8080
targetPort: 80
[root@master ~]# kubectl apply -f httpd.yml
deployment "httpd" created
service "httpd-svc" created
[root@master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpd-78bf969db7-27b2t 0/1 CreateContainerError 0 4s 10.244.1.22 slave1
httpd-78bf969db7-4gx7m 0/1 CreateContainerError 0 4s <none> slave2
httpd-78bf969db7-78nxs 0/1 CreateContainerError 0 4s <none> slave2
httpd-78bf969db7-tgdvd 0/1 Terminating 3 6m 10.244.1.6 slave1
[root@master ~]# kubectl describe pod httpd-78bf969db7-27b2t
報錯Error: Error response from daemon: mkdir /var/lib/docker/overlay/ed1f3b667e779ee685ad979f581e3bb85643c5ce25a627560af9e65c7c8d8fab-init/merged/dev/pts: cannot allocate memory 內存不夠
重新分配內存
[root@master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
httpd-78bf969db7-27b2t 1/1 Running 1 7m 10.244.1.30 slave1
httpd-78bf969db7-4gx7m 1/1 Running 0 7m 10.244.2.13 slave2
httpd-78bf969db7-78nxs 1/1 Running 1 7m 10.244.2.15 slave2
[root@master ~]# kubectl get svc httpd-svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc NodePort 10.107.139.120 <none> 8080:30000/TCP 12m
[root@master ~]# curl -I 192.168.138.130:30000
HTTP/1.1 200 OK
Date: Sat, 29 Sep 2018 12:56:44 GMT
Server: Apache/2.4.35 (Unix)
Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
ETag: "2d-432a5e4a73a80"
Accept-Ranges: bytes
Content-Length: 45
Content-Type: text/html
[root@master ~]# kubectl run busybox --rm -it --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # ping 10.244.1.30
PING 10.244.1.30 (10.244.1.30): 56 data bytes
64 bytes from 10.244.1.30: seq=0 ttl=63 time=0.082 ms
64 bytes from 10.244.1.30: seq=1 ttl=63 time=0.075 ms
64 bytes from 10.244.1.30: seq=2 ttl=63 time=0.054 ms
[root@slave1 ~]# curl -I 10.107.139.120:8080
HTTP/1.1 200 OK
Date: Sat, 29 Sep 2018 12:59:34 GMT
Server: Apache/2.4.35 (Unix)
Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
ETag: "2d-432a5e4a73a80"
Accept-Ranges: bytes
Content-Length: 45
Content-Type: text/html
[root@slave1 ~]# ping 10.244.1.30
PING 10.244.1.30 (10.244.1.30) 56(84) bytes of data.
64 bytes from 10.244.1.30: icmp_seq=1 ttl=64 time=0.047 ms
64 bytes from 10.244.1.30: icmp_seq=2 ttl=64 time=0.055 ms
[root@slave1 ~]# cat policy.yml
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: access-httpd
spec:
podSelector:
matchLabels:
run: httpd
#定義將此 Network Policy 中的訪問規則應用於 label 為 run: httpd 的 Pod,即 httpd 應用的三個副本 Pod。
ingress:
- from:
- podSelector:
matchLabels:
access: "true"
#ingress 中定義只有 label 為 access: "true" 的 Pod 才能訪問應用。
ports:
- protocol: TCP
port: 80
#只能訪問 80 端口
[root@master ~]# kubectl apply -f policy.yml
networkpolicy "access-httpd" created
[root@master ~]# kubectl get networkpolicy
NAME POD-SELECTOR AGE
access-httpd run=httpd 18s
驗證 Network Policy 的有效性
[root@master ~]# kubectl run houpj --rm -ti --labels="access=true" --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ #
/ #
/ #
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.107.139.120:8080)
index.html 100% |*****************************************************************************************************************************************************************| 45 0:00:00 ETA
/ # ls
bin dev etc home index.html proc root sys tmp usr var
/ # ping 10.244.1.30
PING 10.244.1.30 (10.244.1.30): 56 data bytes
^C
--- 10.244.1.30 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
[root@master ~]# kubectl run hou --rm -ti --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.107.139.120:8080)
^C
[root@master ~]# kubectl run hou --rm -ti --image=busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # wget httpd-svc:8080
Connecting to httpd-svc:8080 (10.107.139.120:8080)
^C
[root@master ~]# curl -I 192.168.138.130:30000
^C
[root@master ~]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
busybox-6bdf9b5bbc-4njx5 1/1 Running 0 7m 10.244.1.34 slave1
httpd-78bf969db7-27b2t 1/1 Running 1 1h 10.244.1.30 slave1
httpd-78bf969db7-4gx7m 1/1 Running 0 1h 10.244.2.13 slave2
httpd-78bf969db7-78nxs 1/1 Running 1 1h 10.244.2.15 slave2
[root@master ~]# ping 10.244.1.30
PING 10.244.1.30 (10.244.1.30) 56(84) bytes of data.
^C
--- 10.244.1.30 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 2999ms
集群節點已經不能訪問 Service, 也 Ping 不到副本 Pod。
集群外(192.168.56.1)已經不能訪問 Service。
如果希望讓集群節點和集群外(192.168.56.1)也能夠訪問到應用,可以對 Network Policy 做如下修改:
不管用;
[root@master ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-svc NodePort 10.107.139.120 <none> 8080:30000/TCP 1h
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 18h
[root@master ~]# curl 10.107.139.120:8080
^C
[root@master ~]# curl 192.168.138.167:8080
curl: (7) Failed connect to 192.168.138.167:8080; Connection refused
[root@master ~]# curl 192.168.138.168:8080
curl: (7) Failed connect to 192.168.138.168:8080; Connection refused
[root@master ~]# curl -I 192.168.138.130:8080
curl: (7) Failed connect to 192.168.138.130:8080; Connection refused
Kubernetes 采用的是扁平化的網絡模型,每個 Pod 都有自己的 IP,並且可以直接通信。
CNI 規范使得 Kubernetes 可以靈活選擇多種 Plugin 實現集群網絡。
Network Policy 則賦予了 Kubernetes 強大的網絡訪問控制機制。
參考:cloudman微信公眾號