参考内容:
Kubernetes Certified Application Developer (CKAD) Training | Udemy
引用博客连接附在文章内。
工具网站:
Kubernetes 组件
- API Server: Kubernetes 的前端,用户、管理设备、命令行界面都与 API Server 进行通信,以便与 Kubernetes 集群进行交互
- etcd: Kubernetes 使用的分布式可靠键值存储,用于存储管理集群所用的所有数据,etcd 会以分布式方式将所有信息存储在集群的所有节点上,etcd 也负责在集群内实现锁以确保主设备之间不存在冲突
- Kubelet Server: 集群中每个节点上运行的代理,负责确保容器按预期在节点上运行
- Container Runtime: 用于运行容器的基础软件,实现 CRI 接口即可,可以是 Docker 或其他
- Controller: 负责在节点、容器或端点出现故障时进行通知和响应,并决定是否创建新容器
- Scheduler: 跨多个节点分配工作或容器,查找新创建的容器并将它们分配给节点

Kubernetes 通用配置说明
apiVersion: 创建对象的 API 版本
kind: 创建对象的类型
metadata: 创建对象的数据(只能添加 kubernetes 指定内容)
name: Pod 名称
labels: 可以添加任意键值对
app:
type:
spec: 提供指定信息,例如
containers: 一个列表或数组
- name: Pod 内部容器名称
image: 容器使用的镜像名称
Kind | Version |
POD | v1 |
Service | v1 |
ReplicaSet | apps/v1 |
Deployment | apps/v1 |
CRI/CNI/CSI
CRI 全称 Container Runtime Interface,容器运行时接口,提供计算资源。
CNI 全称 Container Network Interface,容器网络接口,提供网络资源。
CSI 全称 Container Storage Interface,容器存储接口,提供存储资源。
注意以上开放都为接口,代表着分布式操作系统需要的三种基础资源类型。Kubernetes 提供了对以上资源的统筹管理。
Docker 作为最初版的容器服务实现了 CRI 接口,这并不代表 Kubernetes 必须绑定 docker 使用。
网络
Kubernetes 本身不提供功能,通过开放网络接口,由插件实现。
Kubernetes 集群内部存在三类 IP :
- Node IP: 宿主机IP地址
- Pod IP: 虚拟IP,使用网络插件创建的IP(例如 docker容器的IP地址),使跨主机的 Pod 可以互通
- Cluster IP: 虚拟IP,Service 的IP,通过 iptables 规则访问服务
Pod 访问存在几种情况:
- 同 Service 下的 Pod: 直接根据 Pod IP 通信
- 不同 Service 下的 Pod: 借助 Cluster IP
- Kubernetes 集群外的服务: 借助 Node IP
想要了解 Node IP,我们可以使用 kubectl describe node [node_name],观察最下方 Address.InternalIP 的信息

想要了解 Pod IP,可以通过 kubectl describe pod [pod_name],观察 IP

这里顺带提一下,如果使用 hostNetWork 部署,那么 IP 依然为 Node IP

想要了解 Cluster IP,使用 kubectl describe svc [service_name],观察 IP

上篇部署 Kubernetes 时,使用了 flannel。
docker 网络原理
要明白 Flannel 插件的原理,首先要从 docker 的网络原理开始。
我们知道 Linux 网络通过 veth pair 可以连接两个 Network Namespace。而 bridge 可以起到虚拟交换机的作用。docker 默认使用 bridge 网络模式,即通过 bridge 实现一个虚拟的二级网络。
关于 bridge 可以看 Linux虚拟网络设备——bridge

当主机内的容器要和其他主机通信时,根据主机的路由表的直连路由规则,bridge 将请求转交 eth0 处理,请求从而达到其他主机。
上述过程并不能解决一个问题,即 主机A的docker容器与主机B的docker容器如何通信。
下面是解决方案 —— Overlay 网络的设计思路,即通过构建覆盖在宿主机之上的虚拟网来连接所有容器:

以 flannel 的 UDP 模型举例,如下图:

注意 容器/flanneld进程 处于用户态,docker0/flannel0/eth0 处于内核态。由于 UDP 模型的方案涉及多次用户态-内核态转换,多次进行数据拷贝,所以因效率低被废弃。
flannel 插件原理
上面介绍了 flannel 的 UDP 模式,其实已经说明了 flannel 的思路。下面的内容以 flannel 目前默认的 VXLAN 模型为基准,最简单的理解,即 VXLAN 模式将上图的 flanneld 替换成了 VTEP 设备(内核态工作),完成了封包和解包的过程,以 flannel.1 充当网桥角色,进行 UDP 数据包的转发。
flannel 有三个概念:
- 网络(Network): 整个集群中分配给 flannel 要管理的网络地址范围
- 子网(Subnet): flannel 所在的每一台主机都会管理 Network 中的一个子网, 子网的掩码和范围是可配置的
- 后端(Backend): 使用的后端网络模型,如 UDP, VXLAN
在工作时,flannel 主要处理这几件事:
- 配置以二进制文件形式部署在 Node 上
- 为每个 Node 分配子网,容器自动从该子网获取 IP 地址
- 在 Node 加入网络时,为每个 Node 增加路由配置
默认情况下,flannel 使用 etcd 存储网络配置、分配子网和主机公共 IP 信息。首先 flannel 设置集群的整体网络,并在每个 Node 上启动 flanneld 的服务,从 etcd 中读取配置信息,并请求获取子网的租约。从 etcd 获取的信息会写入 /run/flannel/subnet.env 文件。
Flannel 使用 L3 Overlay 模式设计。规定一个 Node 下各个 Pod 同属一个子网,不同 Node 下的 Pod 属于不同的子网。每一个 Node 上会运行名为 flanneld 的进程,负责为 Node 预先分配子网(包括在 ARP 表中添加其他主机 VTEP 设备 IP 对应的 MAC 地址),并为 Pod 分配 IP 地址。
在 Node 节点上,会创建名为 flannel.1 的网卡,并为容器配置名为 cni0 的网桥。这样保证每个 Node 的 cni0 网桥网段在整个集群范围内唯一,从而确保创建的 Pod 的 IP 地址唯一。
Flannel 会修改路由表,实现跨主机通信。
Kubernetes 中使用 flannel 插件传递数据的抽象过程为(下面的 cni0 由 Kubernetes 创建,功能类似 docker0):
- 源容器向目标容器发送数据,首先经过 cni0 网桥
- cni0 网桥转交 flannel.1 虚拟网卡处理
- flannel.1 封装数据,发给宿主机的 eth0
- 由宿主机转发给目标容器宿主机的 eth0
- 目标容器宿主机解包,转发给它的 flannel.1
- flannel.1 转发给 cni0 网桥
- 经由 cni0 网桥到达目标容器
这里 cni0 网桥和 docker 使用 docker0 网桥连接容器原理相同。
只不过 cni0 网桥只对 kubernetes 创建的 Pod 负责(只接管 CNI 插件负责的容器)。Kubernetes 项目并没有使用 Docker 的网络模型,它不具备配置 docker0 网桥的能力。
VXLAN 模型如下图:

通过 flannel.1 构建隧道网络,将 Node 之间的数据传输透明化。对于集群的每个节点,flanneld 进程确保其都能获得其他节点的 VTEP 设备的 MAC 地址(放在 Inner Ethernet Header,通过 ARP 记录获取)和对应节点的 IP 地址(UDP 包的 dst 地址,通过 Forwarding Database 获取)。流程如下:
- 目标容器 IP -> 目标 VTEP IP
- 目标 VTEP IP -> 目标 VTEP MAC (通过 ARP 记录) [ip neigh show dev flannel.1]
- 目标 VTEP MAC -> 目标节点 IP (通过 Forwarding Database) [bridge fdb show flannel.1]
Pod
Kubernetes 最小单元,由一组、一个或多个容器组成,每个 Pod 包含一个 Pause 容器(也称 Infra 容器)作为 Pod 的父容器,负责僵尸进程的回收管理。通过 Pause 容器可以使同一个 Pod 里面的多个容器共享存储、网络、PID、IPC等。

Pause 容器启动后,就可以调用 cni 插件(如 flannel),为这个 Pause 容器的 Network Namesapce 配置网络栈(包括 网卡、回环设备、路由表、iptables 规则)。
关于准备工作
这里提前交代后面出现的一些配置文件的由来和在启动容器之前 Kubernetes 的一些准备。启动 Kubernetes 时,也会启动 kube-flannel,以 daemonSet 方式运行,用于组织跨节点 pod 通信,功能如下:
- 获取 10-flannel.conflist 配置文件(在 kube-flannel 配置文件的 ConfigMap 可配置 cni-conf.json,复制并命名为 10-flannel.conflist)
- 同上一条,获取配置文件 /run/flannel/subnet.env(供后续 /opt/cni/bin/flannel 查询)
- VXLAN 模式下,创建 flannel.1 设备,将设备 MAC 地址和 本节点的IP 记录到节点的注解中
- 启动协程,维护本机路由信息不缺失,防止误删
- 从 etcd 订阅资源,维护路由表项、邻居表项、FDB表项
在部署 kubernetes 的过程中,会在 /opt/cni/bin 路径下安装可执行文件,分为三类:
- 用于创建具体网络设备
- loopback (回环)
- bridge (网桥)
- ptp (veth)
- vlan
- ipvlan
- macvlan
- 分配 IP 地址
- dhcp
- host-local
- 内置 CNI 插件
- flannel
- tuning (调整网络设备参数)
- portmap (通过 iptables 配置端映射)
- bandwith (TBF限流)
关于配置 Pod 网络所处阶段
kubelet 主干代码并不会处理容器网络相关逻辑,而是转交给容器的 CRI 实现去处理,docker 的 CRI 实现名为 dockershim。即使用 dockershim 来调用 docker api 创建并启动 Pause 容器,并将参数传递给 CNI 插件为 Pause 容器配置网络。kubelet 转交 CRI 处理的流程如下:
- RunPodSandBox (创建 Pod,Pause 容器在此被启动)
- ensureImageExists
- netns.NewNetNS (创建 Namespace)
- setupPodNetwork —– CNI addNetwork
- client.newContainer
- Create sandbox container root directories
即在 5新增容器 之前,Pod 网络已经配置完毕了。可以参阅
关于调用的插件和顺序
那么该调用哪些插件呢?
dockershim 通过加载 /etc/cni/net.d/10-flannel.conflist,可以知道第一个调用的插件应为 flannel (/opt/cni/bin/flannel),第二个调用的插件为 portmap (/opt/cni/bin/portmap)。
要调用可执行文件 flannel,需要提供满足 CNI 接口的参数。
CNI 接口如下:
type CNI interface {
AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error
AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
}
主要功能只有两个(其实后续引入了 CHECK 和 VERSION,但这里以早期版本为例):将容器添加到 CNI 网络中 或是 将容器从 CNI 网络移除。
具体可以查看 Container Network Interface Specification
前面的 NetworkConfig 是从 CNI 配置文件中加载到的信息(比如 /etc/cni/net.d/10-flannel.conflist),由具有以下键的 JSON 对象组成:
- cniVersion: 版本
- name: 网络名称,在主机和所有管理域中应为唯一的
- disableCheck: 如果 true,则运行时不能调用此网络配置列表。这允许管理员防止插件组合返回虚假错误
- plugins: CNI 插件和其配置列表,官方示例如下
{
"cniVersion": "1.0.0",
"name": "dbnet",
"plugins": [
{
"type": "bridge",
// plugin specific parameters
"bridge": "cni0",
"keyA": ["some more", "plugin specific", "configuration"],
"ipam": {
"type": "host-local",
// ipam specific
"subnet": "10.1.0.0/16",
"gateway": "10.1.0.1",
"routes": [
{"dst": "0.0.0.0/0"}
]
},
"dns": {
"nameservers": [ "10.1.0.1" ]
}
},
{
"type": "tuning",
"capabilities": {
"mac": true
},
"sysctl": {
"net.core.somaxconn": "500"
}
},
{
"type": "portmap",
"capabilities": {"portMappings": true}
}
]
}
后面的 RuntimeConf 参数包括但不限于:
- CNI_COMMAND: ADD or DEL
- CNI_CONTAINERID: 容器ID
- CNI_NETNS: Pause 容器 Network Namespace 文件路径,或网络命名空间路径
- CNI_IFNAME: 要在容器内创建的接口名称(网卡名称 eth0)
- CNI_ARGS: 通过 Key-Value 传递一些自定义内容
- CNI_PATH: 搜索 CNI 插件可执行文件的路径
回头详细看看 /etc/cni/net.d/10-flannel.conflist:
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
这里要提一下关于 hairpinMode,网桥不允许一个数据包从同一端口收发,除非这个端口开启 Hairpin Mode。开启后,Pod 才可以通过自己配置的 Service 访问到自己。
可以看到在 “type”: “flannel” 后,还有一个 delegate 字段,这意味着 flannel 插件的工作会委托其他 CNI 插件来实现。但目前后面跟着的配置信息明显存在缺失,仅有两条信息,所以 flannel 执行文件还要进行补全,如下:
{
"cniVersion": "0.3.1",
"name": "cbr0",
"type": "bridge",
"bridge": "cni0",
"ipMasq": true,
"hairpinMode": true,
"isDefaultGateway": true,
"ipam": {
"type": "host-local",
"subnet": "10.244.1.0/24",
"dataDir": "/var/lib/cni/",
"routes": [{ "dst": "0.0.0.0/0" }]
}
}
其中,部分配置内容通过查询 /run/flannel/subnet.env 获取。可以查看 subnet.env 做对比

补全之后,就可以调用 bridge 插件了(/opt/cni/bin/bridge)。bridge 的工作流程如下:
- 创建名为 cni0 的 bridge 设备,把子网的第一个地址(如示例:10.244.1.1)绑到 cni0 上
- ip route add 10.244.1.0/24 dev cni0 scope link src 10.244.1.1 (创建路由到 10.244.1.1)
- 创建 veth pair,连接新创建 Pod 的 Namespace 和 cni0 网桥(本文 docker 网络原理图一的网桥结构)
- 为创建的 veth 设置 IP,IP 为 host-local(通过本地文件标记占用 IP,如 /var/lib/cni/networks/cbr0) 分配的值,默认网关设置为 cni0 的 IP 地址
- 设置网卡 MTU
ReplicaSet
为了避免单个 Pod 故障而无法使用的情况,我们希望在 Node 中部署多个 Pod 以提高可用性。ReplicaSet 可以帮助我们在 Kubernetes 集群中运行部件的多个实例。
当 Pod 中只有一个 容器 时,Kubernetes 使用一一对应而不是将多个相同的 容器 放入一个 Pod 中。同理,将维度放大一层,在只有一个 Pod 的情况下,Kubernetes 依然推荐使用 ReplicaSet 对 Pod 进行管理。ReplicaSet 可以在现有 Pod 出现故障时自动启动新单元,从而提供帮助。
ReplicaSet 可以确保始终运行指定数量的 Pod。
重要的一点,ReplicaSet 可以跨集群中的多个 Node 来保证负载均衡,并在需求增加时提供扩展。

关于如何配置 ReplicaSet:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name:
labels:
spec:
template: Pod 相关配置
metadata:
name:
labels:
spec:
containers:
- name:
image:
replicas: 需要的副本数
selector: 选择器,可以帮助 ReplicaSet 确定属于它的 Pod,可以匹配当前 ReplicaSet 启动之前就已存在的 Pod
matchLabels: 标签匹配选择器,匹配 Pod 对应的 labels
app: app1 匹配的标签名和标签的值
Deployment
比 ReplicaSet 更高一级的 Kubernetes 对象。
支持对整体应用的控制,包括扩展环境、修改资源分配、服务暂停、滚动更新、版本回滚。

YAML 配置和 ReplicaSet 类似。
apiVersion: apps/v1
kind: Deployment
metadata:
name:
labels:
spec:
template:
metadata:
name:
labels:
spec:
containers:
- name:
image:
replicas:
selector:
matchLabels:
Namespace
最容易理解的一块,Namespace 同 nacos 等其他工具的命名空间概念高度相似,用于区分不同的工作空间。
创建 Kubernetes 时会自动创建三块 Namesapces:
- default: 默认命名空间
- kube-system: kubernetes 内部用途的 Pod 或服务(网络解决方案、DNS服务等)的命名空间
- kube-public: 放置所有用户都可使用的资源的命名空间
对于(预期)规模较大,较为复杂的项目来说,使用命名空间很重要。有助于在使用同一个 Kubernetes 集群的情况下,隔离各环境所需的资源(比如隔离生产和开发环境)。
命名空间内的资源可以简单的通过名称相互引用(调用)。跨命名空间引用(调用)资源时,需要加上额外的名称,例如:
mysql(同 Namespace) -> mysql.dev.svc.cluster.local (跨 Namespace)
其中,cluster.local 是 kubernetes 集群的默认域名,svc 是服务的子域名,dev 是命名空间。此名称在资源创建时会添加 DNS,因此可以使用此名称跨命名空间访问。
// 使用命令时,请特别注意请求的资源的命名空间
kubectl get pods --namespace=dev
// 改变默认命名空间为指定命名空间
kubectl config set-context $(kubectl config current-context) --namespace=dev
通过 Namespace 创建资源配额,来限制命名空间下的资源用量:
apiVersion: v1 kind: ResourceQuota metadata: name: quota namespace: dev spec: hard: pods: "10" request.cpu: "4" request.memory: 5Gi limits.cpu: "10" limits.memory: 10Gi
ConfigMap
当你有许多 Pod 定义文件时,管理存储在对应文件中的环境变量将变得麻烦。ConfigMap 帮助我们将信息从 Pod 定义文件中提取出,并使用映射(Map)集中管理。
ConfigMap 用于在 Kubernetes 中以键值对的形式传递配置数据,创建 Pod 时,将 ConfigMap 注入 Pod。
// 命令创建 ConfigMap
// 1. 直接创建键值对
kubectl create configmap [config_map_name] --from-literal=[key1]=[value1] \
--from-literal=[key2]=[value2]
// 2. 通过读取文件获取键值对
kubectl create configmap [config_map_name] --from-file=[path_to_file]
// 声明创建 ConfigMap
kubectl create -f [file_name]
apiVersion: v1
kind: ConfigMap
metadata:
name: [config_map_name]
data:
[key1]: [value1]
[key2]: [value2]
在 Pod 定义文件的 spec 内配置 ConfigMap,以获得对应 Map 的环境变量:
# 朴素配置方式
apiVersion: v1
kind: Pod
metadata:
spec:
containers:
- name:
image:
env:
- name:
value:
---
# 注入整个 ConfigMap
apiVersion: v1
kind: Pod
metadata:
spec:
containers:
- name:
image:
envFrom:
- configMapRef:
name: [config_map_name]
---
# 从 ConfigMap 中选择部分注入
apiVersion: v1
kind: Pod
metadata:
spec:
containers:
- name:
image:
env:
- name: [key_name]
valueFrom:
configMapKeyRef:
name: [config_map_name]
key: [key]
---
# 从数据卷中
apiVersion: v1
kind: Pod
metadata:
spec:
containers:
- name:
image:
volumns:
- name:
configMap:
name: [config_map_name]
Secret
Secret 与 ConfigMap 类似。只不过用于保存敏感信息,因而采取不同的编码格式。
同 ConfigMap,Secret 也有命令和声明两种创建方式。
// 命令创建 Secret
// 1. 直接创建键值对
kubectl create secret [docker-registry/generic/tls] [secret_name] --from-literal=[key1]=[value1] \
--from-literal=[key2]=[value2]
// 2. 通过读取文件获取键值对
kubectl create secret[secret_name] --from-file=[path_to_file]
// 声明创建 Secret
kubectl create -f [file_name]
apiVersion: v1
kind: Secret
metadata:
name: [secret_name]
data:
[key1]: [value1]
[key2]: [value2]
不同的是,这里的 value1 和 value2 并不是明文,而是转换为编码格式,使用:
echo -n '[value]' | base64 (编码)
echo -n '[encode_value]' | base64 --decode (解码)
引用同 ConfigMap,将 configMapRef/configMapKeyRef 更变为 secretRef/secretKeyRef 即可。
Secret 中的数据只被编码,而未被加密,同理 etcd 中的数据都未加密,因此要考虑加密静态数据。
关于加密请参照:静态加密 Secret 数据
Docker Security
Docker 主机拥有 1 个 root 用户以及多个非 root 用户。默认情况下, Docker 以 root 用户身份运行容器中的进程。
如果不希望容器中的进程以 root 用户身份运行,则使用 docker run 命令中的 user 设置用户,指定新的用户ID:
docker run --user=1000 ubuntu sleep 3600
另一种加强用户安全性的方法是在创建时在 Docker 镜像中定义此安全性,再构建自定义镜像:
FROM ubuntu
USER 1000
---
docker build -t my-ubuntu-image
---
docker run my-ubuntu-image sleep 3600
Docker 实现了一组安全特性,用于限制容器中 root 用户的能力。即便使用主机 root 用户运行容器,容器中的 root 用户实际上与主机 root 用户不同。
默认情况下,Docker 运行的容器具有有限的功能集,因此在容器中运行的进程没有权限执行以下操作:
- 重启主机
- 可能中断主机或在同一主机上运行的其他容器
如果需要覆盖此行为,并为容器提供可用权限之外的其他权限,可以使用:
// 使用 cap-add 增加权限
docker run --cap-add MAC_ADMIN ubuntu
// 使用 cap-drop 删除权限
docker run --cap-drop MAC_ADMIN ubuntu
// 启用所有权限的情况下运行容器
docker run --privileged ubuntu
Security Context
在 Kubernetes 中,容器被封装在 Pod 中,我们可以选择在 Container级别 或者 Pod级别 配置安全设置。
如果在 Pod级别 配置安全配置,设置将同步到单元中的所有容器。
如果同时在 Container 和 Pod 级别设置了安全配置,Container 级别的优先级会更高。
apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
securityContext:
runAsUser: 1000
containers:
- name: ubuntu
image: ubuntu
command: ["sleep", "3600"]
---
// 注意,capabilities 只能在 container 内添加
apiVersion: v1
kind: Pod
metadata:
name: web-pod
spec:
containers:
- name: ubuntu
image: ubuntu
command: ["sleep", "3600"]
securityContext:
runAsUser: 1000
capabilities:
add: ["MAC_ADMIN"]
Service Account
用途:
- 身份验证
- 授权
- 基于角色的访问控制
Kubernetes 的账户分为两种:
名称 | 使用对象 | 用途 |
用户账户 | 人 | 管理员访问集群以执行管理任务、开发人员访问集群以部署应用 |
服务账户 | 计算机 | 应用程序与集群交互使用,如 Prometheus(监视类应用程序监视运行性能)、Jenkins(自动化构建工具部署应用程序) |
// 创建
kubectl create serviceaccount [account_name]
// 查看
kubectl get serviceaccount
创建 Service Account 时,再自动创建 Token(外部应用在向 Kubernetes API 进行身份验证时必须使用服务账户的 Token),Token 被存储在一个 Secret 中,最后将 Secret 与 Service Account 绑定。
API 在进行 REST 调用时,可以将此 Token 用作身份验证承载令牌(Header 中添加不记名令牌)。
另一种情况,如果第三方程序托管在 Kubernetes 集群本身上,比如我们将自定义 Kubernetes Dashboard 或 Prometheus 应用程序部署在 Kubernetes 集群中。这种情况下,导出 Service Account Token 并配置的过程可以简化为通过将 Secret 作为 卷(Volume)挂载(Mounting)到托管第三方应用程序的 Pod 内。
对于 Kubernetes 的每个命名空间,都会创建一个 default 服务账户。当创建 Pod 时,default 账户及其 Token 都会作为 Volume 自动装载到该 Pod。Secret 安装在 /var/run/secrets/kubernetes。在 Pod 内部执行
ls /var/run/secrets/kubernetes.io/serviceaccount
可以看到 secret 作为 3 个单独的文件挂载。其 token 文件的内容即为 API令牌。
default 账户的限制非常严格,仅具有运行基本 Kubernetes API 查询的权限。要使用其他 Service Account,需要修改 Pod 定义文件(现有 Pod 的 Service Account 无法编辑,修改配置后创建的新 Pod 将会应用新的配置):
apiVersion: v1
kind: Pod
metadata:
name:
spec:
containers:
- name:
image:
serviceAccountName: [account_name]
v1.22 KEP-1205[重要]
KEP-1205 – Bound Service Account Tokens
背景
v1.22的更新提出,当前 JWT 系统存在如下几个问题:
- JWTs 不具备受众约束,任何 JWT 的接收者都可以伪装成任意发送人
- 当前通过 Secret 存储 Service Account Token 并且传递给节点的模式,在运行高权限的组件时,为 Kubernetes 控制带来了广泛被攻击的可能——授予服务帐户权限意味着其他可以看到该服务帐户 Secret 的组件会拥有与该组件一样强大的操作权限
- JWTs 没有时间限制
- JWT 要求每个服务账户有一个 Kubernetes 密钥
动机
引入一种新的机制来匹配 Kubernetes Service Account Token,该机制需要满足安全性和可扩展性要求。
设计
于是 Kubernetes 提出了一种新的设计:
- TokenRequest(API)
- 在 apiserver 实现基础设施,支持根据需求发起 TokenRequest,apiserver的客户端将请求一个弱化的令牌供自己使用,API 将强制执行所需的弱化,例如[受众]和[过期日期]。
- 受众绑定 Audience Binding
- API 发出的 Token 将绑定受众。所请求 Token 的受众将受到 aud声明 的约束。aud声明是与 Token 的预期受众相对应的字符串(通常是URL)数组。Token 的接收者应验证它是否是 aud声明 中的值之一,否则拒绝 Token。TokenReview API 将支持此验证。
- 时间绑定 Time Binding
- API 发出的 Token 将绑定时间。Token 的时间限制包括三个字段:
- exp: 过期时间
- nbf: not before
- iat: issues at
- Token 的接收者应验证 Token 是否有效,否则拒绝 Token。TokenReview API 将支持此验证。
- 集群管理员能够配置 Token 的有效期。在旧版本 Service Account Token 的迁移过程中,此 API 的客户端可能会请求多年有效的 Token。这些 Token 将直接替换当前 Service Account Token。
- API 发出的 Token 将绑定时间。Token 的时间限制包括三个字段:
- 对象绑定 Object Binding
- 从该 API 发出的 Token 可以绑定到与 Service Account 位于同一命名空间中的 Kubernetes 对象。对象的名称、组、版本、种类和 uid 将作为声明嵌入到发布的 Token 中。绑定到对象的 Token 仅在该对象存在时有效。TokenRequest API 将验证此绑定。
以下将加入 authentication.k8s.io API group
type TokenRequest struct {
Spec TokenRequestSpec
Status TokenRequestStatus
}
type TokenRequestSpec struct {
// Token 的意向受众字段
// 为多个访问群体颁发 Token 可用于针对列出的访问群体进行身份验证
// 代表目标受众之间高度信任
Audiences []string
// 请求的有效期
// Token 颁发者可能返回具有不同有效期的 Token
// 因此客户端需要检查响应中的 expiration(过期)字段
ValidityDuration metav1.Duration
// Token 绑定的对象的引用
// Token 仅在绑定对象存在时有效
BoundObjectRef *BoundObjectReference
}
type BoundObjectReference struct {
// 引用的类型,有效类型为 Pod 和 Secret。
Kind string
// 引用的 API 版本
APIVersion string
// 引用的名称
Name string
// 引用的 UID
UID types.UID
}
type TokenRequestStatus struct {
// Token 数据
Token string
// Token 的过期时间,为空意味着不会过期
Expiration metav1.Time
}
TokenReview API 将会被扩展以支持增加的 Audience 字段:
type TokenReviewSpec struct {
// 令牌内容
Token string
// 受众字段
Audiences []string
}
v1.24 KEP-2799[重要]
KEP-2799 Reduction of Secret-based Service Account Tokens
动机
由于 BoundServiceAccountTokenVolume 在 v1.22 中是 GA,因此 pods 的 Service Account Token 将通过 TokenRequest API 获得,并存储为 Volume。这一改变消除了自动生成基于 Secret 的 Service Account Token的需要。
因此在 v1.24 版本中,提案 2799 进行了另一增强。用于减少基于 Secret 的 Service Account Token 的总量。目标有两个:
- 不再自动生成基于 Secret 的 Service Account Token
- 删除不再使用的基于 Secret 的 Service Account Token
设计
LegacyServiceAccountTokenNoAutoGeneration: Token 控制器停止为 Service Accounts 自动创建 Secret。
LegacyServiceAccountTokenTracking: 为促进 LegacyServiceAccountTokenCleanUp,在 kube-apiserver 内实现了一个简单的控制器,控制器在 kube-system(namespace) 内维护一个 bool 值 configMap,用于指示集群中是否启用了跟踪。
LegacyServiceAccountTokenTracking 开启时,控制器在 kube-system(namespace) 内创建/更新一个 configMap 用于存储跟踪开启的日期。当使用旧令牌时,发出警告,更新 Secret 内的 last-used 标签,并记录在度量中。
LegacyServiceAccountTokenTracking 关闭时,控制器保证 kube-system(namespace) 中的 configMap 周期性的方式被删除。
LegacyServiceAccountTokenCleanUp: Token 控制器清除不使用且不再被 Pod 装载的 自动生成的 Secret。当此功能默认启用时,如果自 last-used 后超过了足够的时间(默认为一年),则删除 Secret。该时间段可以由集群管理员配置。
确定 Secret 的 last-used 的日期:
- last-used 存在 且 在 tracked-since 之后
- 默认为 tracked-since
如果 tracked-since 不可用,那么就不会删除任何 Secret。
简单总结
v1.22 之前的流程:
- 创建 Service Account
- 自动创建带有 Token 的 Secret
- 使用此 Service Account 创建一个 Pod,Secret 作为 Volume 挂载到使用该 Service Account 的 Pod(Pod 中的 Token 和 Service Account 中的相同)
v1.22 及之后可以通过 TokenRequest API 获取 Token:
- 创建 Service Account
- 自动创建带有 Token 的 Secret(同样不过期)
- 使用此 Service Account 创建一个 Pod,但是 Volume 挂载到 Pod 中的 Token 值和 Service Account 中的不同,其中大致分为以下:
- kubelet 通过 TokenRequest API 获取弱化 Token,该 Token 会在 Pod 被删除 或 生命周期结束(默认 1 小时)后过期,过期后会去重新申请
- 绑定该 Token 至对应的 Pod,将其 Audiences 设置为与 TokenRequest API 中一致
- 维护 configMap,Pod 以此确保自己连接到 apiserver
- 手动请求 TokenRequest API 获得 Token(kubectl create token [service_account_name 可选])
v1.24 及以后,不再自动创建 Secret:
- 创建 Service Account
- 不再创建 Secret,但会自动挂载 Volume,Volume 内包含了 API 产生的 Token(默认有效期 1 年)
- 使用此 Service Account 创建 Pod 后,和 v1.22 之后的流程相同
v1.24 及之后,如果需要配置不过期的 Token:
- 创建 Service Account
- 获取 Token,创建 Secret,配置如下:
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
name: [secret_name]
annotations:
kubernetes.io/service-account.name: [service_account_name]
只有在无法使用 TokenRequest API 获取 Token 的情况下,才应该创建 Service Account Token Secret。
Resource Requirements
对于一个 Kubernetes 集群,其中每个 Node 都拥有一组可分配的资源,包括 CPU、内存空间、硬盘空间,而 Pod 会消耗这些资源。只要将 Pod 部署在 Node 上,就会开始占用该 Node 的可用资源。
Kubernetes Scheduler 会考虑 Pod 所需的资源量和 Node 可用的资源量,来决定 Pod 应该部署在哪个 Node 中。如果任何 Node 内都没有足够的可用资源,Kubernetes 将阻止调度端口,会导致端口处于挂起状态,可以在 Events 中看到 FailedScheduling 事件。
apiVersion: v1
kind: Pod
metadata:
name: ubuntu
spec:
containers:
- name: ubuntu
image: ubuntu
resources:
requests:
memory: "1Gi"
cpu: 1
limits:
memory: "2Gi"
cpu: 2
首先要注意,resources 字段是放在 container 下的,因此它对 容器 作限制。
我们通过 resources 下的 requests,来设置当前 容器 需要请求的最小资源量。
在 Docker 中,容器 消耗 Node 的资源不受限制,比方说容器开始运行时只占用 1 vCPU,它可以一直增加直到占据所有可用 CPU 资源,从而阻塞 Node 上的其他本地进程或影响其他容器。
因而我们通过在 resources 下定义 limits,来设置当前 容器 的资源使用上限,0 表示无上限。
上述定义中的 CPU计数 代表一种占用的概念,并非必须为整数。1 也可以表示为 1000m,其中 m 代表 millicores,为 CPU 时间,即 单位CPU时间 细分为1000份 分配给某容器。
1 CPU计数 可以用于表示 1 个 AWS vCPU、1 个 GCP 核心、1 个 Azure 核心、或者 1 个超线程。
1G (Gigabyte) | 1,000,000,000 bytes |
1M (Megabyte) | 1,000,000 bytes |
1K (Kilobyte) | 1,000 bytes |
1Gi (Gibibyte) | 1,073,741,824 bytes |
1Mi (Mebibyte) | 1,048,576 bytes |
1Ki (Kibibyte) | 1,024 bytes |
当 容器 尝试超过限制去获取资源时,如果该资源为 CPU,则 CPU 将会对该 容器 限流。如果该资源为内存,则 容器 将会被 OOM 关闭,从而发生重启。
如果当前容器的内存占用未达上限,但 Node 的内存达到了上限,也会触发 OOM 杀死容器。
Taints & Tolerations
Taints 在 Node 级别设置,而 Tolerations 在 Pod 级别设置。
如果 Node 设置了 Taints,那么未设置对应 Tolerations 的 Pod 将无法分配到 Node 上。
要给 Node 设置 taints 可以通过:
kubectl taint nodes [node_name] [key]=[value]:[taint_effect like NoSchedule|PreferNoSchedule|NoExecute]
// e.g.
kubectl taint nodes node1 app=blue:NoSchedule
taint effect:
- NoSchedule: 不会在 Node 上部署 Pod
- PreferNoSchedule: 尝试避免在 Node 上部署 Pod
- NoExecute: 不执行,Pod 将不会在 Node 上调度,且如果 Node 上的现有 Pod 不能容忍 taint,将被逐出
要给 Pod 设置 tolerations 可以通过修改 Pod 定义文件:
注意!!!tolerations 中的配置内容请用双引号包含
apiVersion: v1
kind: Pod
metadata:
name: ubuntu
spec:
containers:
- name: ubuntu
image: ubuntu
tolerations:
- key: "app"
operator: "Equal"
value: "blue"
effect: "NoSchedule"
关于 taints 和 tolerations 还需注意一点。Pod 设置了 tolerations,说明它具备了在相同 taints 配置的 Node 中部署的能力,但不代表此 Pod 一定会在对应的 Node 中部署,此 Pod 依然可以在其他 Node 中部署。
想要 Pod 运行在指定的 Node 上,应该使用 Node Affinity。
关于为什么 Master Node 未被分配 Pod,同样是因为在首次设置 Kubernetes 集群时,会在 Master Node 上自动设置一个 taint,以防止在此 Node 上调度任何 Pod。可以通过以下命令观察:
kubectl describe node [master_name] | grep Taint
Node Selectors
如果想要对 Pod 加以限制,使其仅在指定 Node 上运行,可以使用 Node Selectors:
apiVersion:
kind: Pod
metadata:
name: myapp-pod
spec:
containers:
- name: data-processor
image: data-processor
nodeSelector:
size: Large
可以看到示例中通过 size: Large 来匹配要要放置 Pod 的正确节点。所以不但要在 Pod 中进行配置,我们也要对 Node 进行标记:
kubectl label nodes [node_name] [label_key]=[label_value]
// e.g.
kubectl label nodes node-1 size=Large
Node Selectors 的匹配过于简单,因而并不一定能满足需求,如果 size 包括 {Large, Medium, Small},我们要匹配不为 Small 的两种情况,就无法使用 Node Selectors 实现,需要依赖 Node Affinity。
Node Affinity
Node Affinity 特性的主要目的是确保 Pod 部署在特定的 Node 上(例如多不同性能 Node 的情况下,某些大开销 Pod 需要分配到高性能 Node 中)。Node Affinity 提供了高级功能,以处理复杂于 Node Selectors 的情况。对于 Node Selectors 后面提到的情况:
apiVersion:
kind: Pod
metadata:
name: myapp-pod
spec:
containers:
- name: data-processor
image: data-processor
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: size
operator: NotIn
values:
- Small
这个示例里面包含的内容会比其他的功能要多,不要紧张,这里会慢慢说明。
考虑一种情况,即 Node 上的标签被修改,假如 Pod 根据 Node Affinity 运行在带有 size=Small 的 node-1 上,有人修改了 node-1 的标签 size=Small 为 size=Large 该怎么办呢?
假如匹配上的 Node 处于不可用状态或者根本不存在被匹配上的 Node,又该怎么办呢?
The type of node affinity defines the behavior of the cellular with respect to node affinity and the stages in the life cycle of the pod.
nodeAffinity 下的长句部分(被称为 Node Affinity 的类型)定义了 Pod 生命周期各阶段关于 Node Affinity 的行为。如下:
- Available 实装的:
- requiredDuringSchedulingIgnoredDuringExecution
- preferredDuringSchedulingIgnoredDuringExecution
- Planned 计划中未实装:
- requiredDuringSchedulingRequiredDuringExecution
Node Affinity 类型主要关注两个阶段,即 Pod 生命周期中的 调度阶段(Scheduling)和 执行阶段(Execution)。
DuringScheduling | DuringExecution |
Required | Ignored |
Preferred | Ignored |
Scheduling 阶段是 Pod 并不存在并且初次被创建的阶段。毫无疑问,在 Pod 被初次创建时,Node Affinity 会将其部署到对应的 Node 上。
如果 DuringScheduling 为 Required,则调度程序将强制将 Pod 放置到对应规则(标签)的 Node 上,如果找不到,则不会调度(部署) Pod。这种类型用于 Pod 部署的 Node 位置比顺利运行起 Pod 更重要时(例如只有某个 Node 适合部署当前 Pod)。
如果 DuringScheduling 为 Preferred,则调度程序若没有找到匹配的 Node,将会忽略 Node Affinity,将 Pod 部署在任何可用的 Node 上。即,顺利运行起 Pod 比 Pod 所部署的 Node 位置更重要。
Execution 阶段是 Pod 执行期间,即 Pod 已经运行时的阶段。此时 Node 进行了 Node Affinity 修改(如上面提到的,将 size=Large 修改为 size=Small)。
如果 DuringExecution 为 Ignored,即 Pod 将忽视这些 Node 变更,继续运行而不做任何行为。
如果 DuringExecution 为 Required,则 Node修改 Labels 会将其对应的 Pod 逐出。
这里做一个简单的总结:
Taints (node) – Tolerations (pod) 和 Labels (node) – Node Affinity (pod) 都需要在 Node 和 Pod 两边配置。
使用的区别是,Taints – Tolerations 用于排斥某些 Pod 部署在 Node 上,而 Node Affinity 用于吸引某些 Pod 部署在 Node 上。
Multi-Container Pods
多容器 Pod,常见的情况例如 Web Server 和 Log Agent,又或是 Server 和 SkyWalking Agent。
多容器 Pod,共同创建和销毁,共享相同的网络空间,并且可以访问相同的存储卷(如 SkyWalking Agent 的 SideCar 引入方式)。这样就不必在 Pod 之间建立共享卷或服务来启用他们之间的通信。
Multi-Container Pod 和 普通 Pod 的区别是,spec 部分下的 Container 部分是一个数组,如下:
apiVersion: v1
kind: Pod
metadata:
name: mul-container-pod
spec:
containers:
- name: simple-webapp
image: simple-webapp
ports:
- containerPort: 8080
- name: log-agent
image: log-agent
通常设计一个 Multi-Container Pod 有多种模式,比如:
- SideCar
- Adapter
- Ambassador
SideCar 模式例如上述给 Web 应用增加 Log Agent,用于收集日志转发到中心日志服务。SkyWaling 官方也推荐使用 SideCar 的方式进行配置。
Adapter 模式,考虑到各 Pod 中的服务模块产生的 Log 格式不一定相同,如果想要将不同格式风格的 Log 转化为统一标准,可以使用 Adapter 方式,配置一个 Adapter 容器。Adapter 容器可以在将日志发送到中心日志服务之前对其进行处理。
Ambassador 模式用于处理另外一种情况,比如应用程序在不同开发环境可能与不同的数据库实例进行通信,通常我们将数据库相关信息放置在代码的配置文件中,这时也可以选择将此类逻辑抽离到 Pod 中的单独容器。
Observability
Kubernetes 的可观测性,包含:
- Readiness Probes
- Liveness Probes
- Container Logging
- Monitor And Debug Applications
Pod Status
我们通常通过 Pod Status 来观察 Pod 处于其生命周期的哪个状态。
通常包含以下三个状态:
- Pending
- ContainerCreating
- Running
第一次创建 Pod 时,其通常处于 Pending 状态。此时 Scheduler 会尝试找出能够 Pod 的节点,如果 Scheduler 找不到能够放置 Pod 的节点,它将始终保持 Pending 状态。通过 describe pod 命令,可以查看到处于 Pending 状态的确切原因。
一旦 Pod 被安排到任一节点,就将进入 ContainerCreating 状态。在此状态下,将会去尝试拉取 Image,并且启动 Container。一旦 Pod 中所有的 Container 都启动成功,就会进入并一直处于 Running 状态,直到程序成功完成或终止。
通过 kubectl get pods 可以看到 Pod 状态。
Pod Conditions
当然,Pod Status 提供的信息是高度抽象的,所以 Kubernetes 还提供了 Pod Conditions。
Pod Conditions 通常为一组布尔数组。告诉我们 Pod 当前的 State(状态),如下:
- PodSchedued
- Initialized
- ContainersReady
- Ready
当 Pod 被调度到节点时,PodSchedued 将由 false 更新为 true。同理,当 Pod 被初始化完成后,Initialized 将由 false 更新为 true。当所有的 Container 都准备就绪后,ContainersReady 将更新为 true。最后,Pod 被认为准备就绪,Ready 更新为 true。
通过 kubectl describe pod 可以看到 Conditions 部分。通过 kubectl get pods 也可以看到当前 Pod 是否 Ready。
为什么要讨论 Pod 的 Status 和 State。因为在很多情况下,Pod 的状态虽然转变为 Ready,但此时服务并未可以正常使用。通常的 SpringBoot 后端服务启动可能需要数秒到数十秒的时间,而在此期间 Pod 的状态都将是 Ready。服务不会意识到这一点,如果仅根据 Ready 就将流量发送至 Pod,将会使用户访问到尚未运行成功的 Pod。
Readiness Probes
此时我们需要一种,将 Ready 条件与容器内应用程序的实际状态关联起来的方法。我们可以使用很多种不同的方式来定义 Container 内的应用程序是否已经实际就绪,例如:
- 对于 SpringBoot 后端应用,可以尝试 HTTP TEST /actuator/health
- 对于 数据库,可以尝试 TCP TEST 3306
- 也可以通过自定义脚本检测
- 通过 initialDelaySeconds 为 Probe 增加额外的延迟
- 通过 periodSeconds 设定 Probe 探测的时间间隔(频率)
- failureThreshold 设定失败几次后停止(默认为三次)
下面通过示例配置 Readiness Probes
apiVersion: v1
kind: Pod
metadata:
name: simple-webapp
spec:
containers:
- name: simple-webapp
image: simple-webapp
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
tcpSocket:
port: 3306
exec:
command:
- cat
- /app/ready
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 5
Liveness Probes
如果说 Readiness Probe 是用来检测 Container 启动时,服务是否准备就绪的话,那么顾名思义 Liveness Probe 就是用来检测服务是否还存活的。
假设一种情况,我们用 Docker 运行 Nginx 镜像,由于某种原因,Web 服务器崩溃,Nginx 进程退出,容器也退出。我们使用 docker ps -a 命令时,就可以看到容器的 Status 处于 Exited。这里的问题是,Docker 并不是编排(Orchestration)引擎,容器退出后会一直处于停用状态,无法向用户提供服务。
使用 Kubernetes 运行同样的镜像时,Kubernetes 就会在上面的情况发生时尝试重启容器以恢复服务。可还是存在一种情况,即服务依然无法正常运行,但容器仍然保持活动状态。
Liveness Probe,可以帮助我们定期测试容器内的服务是否正常。Liveness Probe 与 Readiness Probe 非常相似,所以我们直接给出示例:
apiVersion: v1
kind: Pod
metadata:
name: simple-webapp
spec:
containers:
- name: simple-webapp
image: simple-webapp
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
tcpSocket:
port: 3306
exec:
command:
- cat
- /app/health
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 5
Container Logging
Kubernetes 提供多种日志机制。
在使用 Docker 的情况下,我们通过 docker logs -f [container_id] 跟踪容器产生的标准输出流日志。
使用 Kubernetes 时,我们使用类似的
kubectl logs -f [pod_name]
当然,这些日志特定于在 Pod 内部运行的容器。上面我们说到过,Pod 中可以有多个容器,那么在多个容器都输出日志的情况下,想要查看某一容器的日志,我们需要在命令中显式指定容器的名称,如下:
kubectl logs -f [pod_name] [container_name]
Monitor & Debug Applications
为了监控 Kubernetes 上的重要信息,例如资源消耗、节点运行情况、性能指标,我们需要一个解决方案来监控存储这些指标。Kubernetes 还未配备全功能的内置监控解决方案,然而目前有多种开源方案,比如:
- Metrics Server
- Prometheus
- The Elastic Stack
- Datadog
- Dynatrace
这里介绍 Metrics Server。
Hipster 是 Kubernetes 提供监控和分析功能的原始项目之一。Hipster 目前已被弃用,并形成了一个精简版本,就是 Metrics Server。每个 Kubernetes 集群可以有一个 Metrics Server,此服务从每个节点和单元中检索指标,将其聚合并存储在内存中。所以请注意 Metrics Server 是一个依赖于内存的监控解决方案,并不将指标存储在磁盘上(持久化),因此无法查看历史数据。
我们知道 Kubernetes 在每个节点运行 Kubelet,用于接受 Api Server 的指令并在节点上运行 Pod。Kubelet 也包含一个 cAdvisor 或 Container Advisor 的子组件,cAdvisor 负责从 Pod 检索性能指标,并通过 Kubelet Api 公开这些指标,使得 Metrics Server 能够使用这些指标。
关于 Metrics Server 的部署,可以查看《云原生实践:内网构建 Kubernetes 与 docker 环境》
Pod Design
Labels & Selectors & Annotations
标签,选择器是用来将事物组合在一起的标准方法。
我们有多个不同的物品,可以依据不同的标准对他们进行过滤。
比如通过颜色、口味、形状来区分各种水果。
使用 绿色、甜、球形,我们就可以从多种水果里筛选出西瓜。
在 Kubernetes 中也是同理,我们可以给 Pod 设定各种各样的标签,也可以给选择器设置多种条件从而筛选出我们想要的 Pod。
我们在 metadata 下,可以定义 labels:
apiVersion: v1
kind: Pod
metadata:
name: watermelon
labels:
color: green
taste: sweet
shape: spherical
spec:
containers:
- name:
image:
env:
- name:
value:
此时,在一些命令中,就可以通过 selector 进行筛选:
kubectl get pods --selector color=green
这里要注意一个细节,即 labels 是打在当前所属的 metadata 对象下的,这里举例:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: replicaSetName
labels:
app: app1
spec:
template:
metadata:
name: podName
labels:
app: app1
spec:
containers:
- name:
image:
replicas: 3
selector:
matchLabels:
app: app1
这里上面 name: replicaSetName 下面的标签,是 ReplicaSet 的标签。而 name: podName 下的标签,才是 Pod 的标签。这里我们为了让 ReplicaSet 连接到 Pod,这里在 replicaSet 的 spec 下,使用 Selector 匹配了 app: app1 这个标签。用于匹配 Pod 中定义的相同标签。
同理,在创建 Service 时,将使用 Service 配置中 spec 下面的 Selector 来匹配 ReplicaSet 中 template 下 Pod 的标签。
最后来看 Annotations。注释用于记录其他细节以提供信息。例如版本,联系人,电子邮件等,如下:
apiVersion: v1
kind: Pod
metadata:
name: app
annotations:
buildversion: v1.0.0
Rollout & Versioning
当我们创建或修改 Deployment 时(比如 kubectl apply -f 等),会触发一个 Rollout。每一个新的 Rollout 都会创建新的 Deployment Revision。当下一次应用发布时,意味着容器镜像版本将更新为一个新的值,这将会触发新的 Rollout,从而创建新的 Deployment Revision。
Rollout 帮助我们跟踪 Deployment 发生的改动,并允许我们在必要时将 Deployment 回滚到先前的版本。
# 查看 Rollout 状态
kubectl rollout status deployment/[deployment_name]
# 查看 Revisions 和 Rollout 的历史信息
kubectl rollout history deployment/[deployment_name]
例如,你有一个 Web 应用通过 Deployment 部署,其下包含 5 个 replicas。
目前对于 Deployment 的更新有两种策略。
- Recreate: 先销毁所有的旧版本 Pod,再创建最新版本的 Pod。这种策略的结果显而易见,在新旧更替之间的一段期间,应用是无法被用户访问的。
- Rolling Update: 滚动升级作为默认策略,逻辑是先销毁一个旧版本 Pod,再拉起一个新版本的 Pod,销毁与创建交替进行,通过这种策略更新版本将不会被用户感知到。
通过 kubectl set image deployment/[deployment_name] [image_name]=[image_name]:[image_version] 也可以更新镜像版本,但是会导致本地配置文件和真实环境中的配置不匹配。后续使用配置文件时容易遗漏出现问题。
关于 Deployment 更新的具体逻辑,首先明确一点是,Kubernetes 并不是在同一个 ReplicaSet 中进行 Pod 的更新。而是在不销毁原有 ReplicaSet 的基础上,创建一个新的 ReplicaSet,我们将其称为旧集合和新集合。旧集合销毁 n 个 Pod 后,就在新集合拉起 n 个 Pod,直到旧集合的 Replicas 变为 0。
# 回滚更新
kubectl rollout undo deployment/[deployment_name]
Job
在 Kubernetes 中,我们不仅仅需要长期运行的应用,也需要单次执行的任务,例如:批处理、分析数据集或生成报告。这些工作负载的生存周期较短,通常仅为执行一组任务并完成。
首先我们要知道在 Docker 中是如何处理这一类任务的。可以尝试如下命令:
docker run ubuntu expr 3 + 2
Docker 会拉起容器,执行请求的操作,打印输出后退出。使用 docker ps 可以看到,容器将处于退出状态,执行操作的返回代码为 0(表示任务已成功完成)。
我们在 Kubernetes 中模拟同样的操作,可以创建以下配置:
apiVersion: v1
kind: Pod
metadata:
name: math
spec:
containers:
- name: add
image: ubuntu
command: ['expr', '3', '+', '2']
restartPolicy: Never
当 Pod 创建时,它会运行容器,执行计算任务并退出,但请注意,Pod 的默认行为会尝试启动容器以保持其处于运行状态,所以此 Pod 退出后会被不停重启,直到达到设定的阈值(restartPolicy 默认为 Always,应修改为 Never)。
假设我们现在需要多个 Pod 来并行处理数据,我们希望所有 Pod 成功执行分配给他们的任务,然后退出。因此我们需要一个 Manager,可以创建尽可能多的 Pod 来执行任务,并确保任务成功完成。
我们已经学过 ReplicaSet 可以用于管理 Pod,并确保指定数量的 Pod 始终处于运行状态。相对应的,Job 用于运行一组 Pod 来完成给定任务。
apiVersion: batch/v1
kind: Job
metadata:
name: add-job
spec:
completions: 3
parallelism: 3
template:
spec:
containers:
- name: add
image: ubuntu
command: ['expr', '3', '+', '2']
restartPolicy: Never
使用 kubectl create -f 创建上述 Job 后,通过 kubectl get jobs 可以看到,STATUS 转变为 Completed,意味着没有尝试重新启动 Pob。
Job 可以通过 completions 来控制完成的 Pod 的数量,例如设置为 3,则要求必须有 3 个 Pod 顺利完成了任务。通过 parallelism 来控制并行创建 Pod,例如设置为 3,则每次同时创建 3 个 Pod。如果第一轮创建的 3 个 Pod 中有 2 个顺利完成了任务,则后续第二轮开始每轮只创建 1 个 Pod,直到有 3 个 Pod 顺利完成。
CronJob
CronJob 类似于 Linux 中的 Crontab 任务调度。我们知道创建 Job 后,任务会立即执行,相对应的,我们可以通过创建 CronJob 定期调度和运行任务。
apiVersion: batch/v1
kind: CronJob
metadata:
name: add-job
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
completions: 3
parallelism: 3
template:
spec:
containers:
- name: add
image: ubuntu
command: ['expr', '3', '+', '2']
restartPolicy: Never
Service
Service 支持应用程序内部和外部的各种组件之间的通信,帮助我们将应用程序与其他应用程序或用户连接在一起。
例如,我们的应用程序有多组运行不同部分的 Pod:第一组用于前端负载,第二组用于后端进程,第三组连接到外部数据源,而 Service 实现了这些组之间的连接。Service 使前端应用程序可供用户使用,构建前端与后端之间的通信,也能够帮助后端与外部数据源进行连接。
Service 类似于网络层的接口,因此支持微服务之间的松散耦合。

由于 Kubernetes 集群采用虚拟网,在外部访问的视角下,Pod 的 IP 地址和端口通常并不是我们想要的。
例如,我们通常希望通过服务器的 IP 地址访问到集群内 Pod 的前端页面,所以在服务器 IP 和虚拟网 IP 之间,需要一层映射,这时就需要用到 Service(NodePort)。
Service 有以下几种类型:
- NodePort:映射 Node 的 IP:端口到 Pod 的 IP:端口(基于 ClusterIP,即公开集群内部服务)
- ClusterIP:在集群内分配一个虚拟IP,以支持不同服务的通信
- LoadBalancer:由云厂商提供负载均衡
NodePort
前面我们说到,NodePort 可以实现 Node IP:端口 到 Pod IP:端口 之间的映射。
我们完善上面的例子,假设现在有:
Pod – IP 为 10.244.0.2 开放端口 80
Node – IP 为 192.168.1.10 开放端口 30080 (NodePort 仅支持高位端口)
所以配置 Service 时,Pod 的 80 端口是 TargetPort(目标端口,也就是转发的目的地)
Service 具有集群内的虚拟IP和自己的端口,简称为 Port。
最后,Node 具有一个开放的高位端口(30000 – 32767),也是用户请求进入集群服务的入口,称为 NodePort。

下面是对应的配置:
apiVersion: v1
kind: Service
metadata:
name: front-end
spec:
type: NodePort
ports:
- targetPort: 80
port: 80
nodePort: 30080
selector:
app: app
type: front
在这个配置中,port 是必填内容,targetPort 为空默认与 port 相同,而 nodePort 为空则自动分配空闲端口。
这时 Service 已经明确了要映射的端口相关信息,但是如何知道要映射到哪个 Pod 下的对应端口呢?
答案是使用 Label 和 Selector。通过 app 和 type 标签,Service 会将请求转发至对应 Pod 的 80 端口。注意,通过使用标签和选择器,Service 能够指定多个 Pod。
需要补充说明的是,NodePort 将会映射所有节点的对应端口。也就是上述配置完成后,访问集群中所有节点对应 IP 的 30080 端口,都将会转发到对应内部 Pod 的 80 端口中。
当 Pod 被添加或移除时,Service 会自动更新,使其具有高度的灵活性和适应性。
ClusterIP
我们知道,每次创建 Pod 时将会给它分配一个虚拟网IP地址。当前端想要访问服务后端时,如果通过 Pod 的 IP 进行定位,一旦 Pod 销毁,将会出现错误。因此 Service 类似于一个会动态更新的代理。另外一种情况,Service 能够有序地分配请求到多个 Pod。
有了 Service 之后,Pod 之间的服务请求不再直接通过 IP 地址,而是先发送至 Service 再由 Service 分配。Service 不但会被分配到 IP,同样要考虑 Service 可能存在被销毁的情况,因此 Service 还拥有名称,这样各个 Pod 可以通过名称定位到当前有效的 Service。
这种在集群内部的 Service 类型,称为 ClusterIP。和 NodePort 类似,配置如下:
apiVersion: v1
kind: Service
metadata:
name: back-end
spec:
type: ClusterIP
ports:
- targetPort: 80
port: 80
selector:
app: app
type: back
Ingress
学习 NodePort 后,我们已经知道如何将 Kubernetes 集群内的端口通过高位端口映射到节点外部。例如现在集群内部署了 3 类服务,我们称为服务 A,服务 B 和 服务 C。通过 NodePort,我们当然可以将这三个服务都通过 Node 的高位端口暴露出去,再在集群外通过 Nginx 来管理,如图所示:

Kubernetes 的 Ingress,解决了需要在外部配置网关或代理转发的问题,通过 Ingress,上述情况就变成了下图:

Ingress 分为两部分,解决方案为 Ingress Controller,就是选择代理的工具,通常包括:
- GCE(Google 的 Load Balancer)
- Nginx
- Contour
- Haproxy
- Trafik
- Istio
规则集称为 Ingress Resources,即我们配置的路由规则。
Kubernetes 集群在默认情况下没有 Ingress Controller,也就是说仅创建 Ingress Resources,Ingress 并无法工作。
这里 Ingress Controller 的 Nginx 是 Nginx 的特殊构建版本,它拥有额外的功能,可以监控 Kubernetes 集群的新配置和 Ingress Resources,并相应的配置 Nginx 服务器。配置如下:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-ingress-controller
spec:
replicas: 1
selector:
matchLabels:
name: nginx-ingress
template:
metadata:
labels:
name: nginx-ingress
spec:
containers:
- name: nginx-ingress-controller
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0
args:
- /nginx-ingress-controller
- --configmap=${POD_NAMESPACE}/nginx-configuration
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-configuration
---
apiVersion: v1
kind: Service
metadata:
name: nginx-ingress
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
- port: 443
targetPort: 443
protocol: TCP
name: https
selector:
name: nginx-ingress
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-ingress-serviceaccount
上面的配置文件,创建了 Deployment、ConfigMap(用于配置 nginx)、Service(NodePort,对外暴露 Ingress)、ServiceAccount(拥有正确权限的服务账户)。
下面我们来配置 Ingress Resources,有三种类型:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress
spec:
backend:
serviceName: service-a
servicePort: 80
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress
spec:
rules:
- http:
paths:
- path: /a
backend:
serviceName: service-a
servicePort: 80
- path: /b
backend:
serviceName: service-b
servicePort: 80
- path: /c
backend:
serviceName: service-c
servicePort: 80
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress
spec:
rules:
- host: a.wujunhao1024.com
http:
paths:
- backend:
serviceName: service-a
servicePort: 80
- host: b.wujunhao1024.com
http:
paths:
- backend:
serviceName: service-b
servicePort: 80
Network Policy
通常情况下,Kubernetes 集群内的各 Pod 之间访问是无限制的。通过 Network Policy,可以限制各组件之间的通信,以符合安全标准。
Network Policy 是 Kubernetes 命名空间中的一种对象,与 Deployment、ReplicaSet 和 Service 类似,可以将 Network Policy 连接到一个或多个组件。通过在 Network Policy 中定义规则,可以限制组件的入口流量和出口流量,只允许指定规则的通信。
Network Policy 绑定组件的方式和 ReplicaSet、Service 等类似,都是通过标签和选择器。区别是,在 Network Policy 中的选择器名称为 podSelector。Network Policy 可以指定 Ingress(入口流量) 或 Egress(出口流量),下面给一个示例:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
name: api-pod
namespaceSelector:
matchLabels:
name: prod
- ipBlock:
cidr: 192.168.5.10/32
ports:
- protocol: TCP
port: 3306
spec 下的 podSelector 用于选择被 Network Policy 保护的 Pod。而 ingress 下的 podSelector 用于选择作为流量来源或流量目的地的 Pod。namespaceSelector 用于选择想匹配的命名空间(前提是命名空间上已经配置了对应的标签)。
podSelector 和 namespaceSelector 都是可选的,podSelector 用于限制对应标签的 pod,而 namespaceSelector 用于选择对应标签命名空间下的 pod。
第三个选择器是 ipBlock,用于限制一个范围的 ip 地址的流量。
示例配置中还有一个细节:
- from:
- podSelector:
matchLabels:
name: api-pod
namespaceSelector:
matchLabels:
name: prod
- ipBlock:
cidr: 192.168.5.10/32
需要注意 from 下有两个列表,第一个列表包含 podSelector 和 namespaceSelector,意味着允许同时满足两个限制的流量,而第二个列表只有 ipBlock,意味着只要满足 ipBlock 限制,流量便可通过。列表之前的逻辑关系是或,而同列表下的关系是与。
注意的是,只有在策略类型中填写了 Ingress, Egress 时,入口或出口限制才会生效。上述示例中只填写了 Ingress,意味着出口流量将不受影响。
这里我们同样给出出口流量的配置:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 192.168.5.10/32
ports:
- protocol: TCP
port: 80
Network Policy 是由 Kubernetes 集群上实施的网络解决方案强制执行的,并非所有网络解决方案都支持 Network Policy。支持 Network Policy 的有:
- Kube-router
- Calico
- Romana
- Weave-net
而 Flannel 不支持 Network Policy。
State Persistence
由于 Docker 的特性,容器可能只能存在一段时间,当需要处理数据时,它们会被调用,并在完成后销毁。为了保存容器处理的数据,我们在创建容器时将卷附加到容器,这时容器处理后的数据可以放到 Volumes 中,在容器销毁后 Volumes 中的内容不会受影响。
Kubernetes 同理,可以通过挂载 Volumes 给 Pod 分配持久化存储区块。下面是一个示例:
apiVersion: v1
kind: Pod
metadata:
name: random-number-generator
spec:
containers:
- image: alpine
name: alpine
command: ["/bin/sh", "-c"]
args: ["shuf -i 0-100 -n 1 >> /opt/number.out;"]
volumeMounts:
- mountPath: /opt
name: data-volume
volumes:
- name: data-volume
hostPath:
path: /data
type: Directory
上面的示例中,配置了一个 Volume,直接使用主机路径作为 Volume 的存储空间。该方式作为最简单的 Demo,不推荐在多集群情况下使用,因为每个 Pod 都将会在所属服务器下对应路径创建文件,各个 Pod 存储的内容是不一致的。通常选择使用供应商提供的弹性块存储,例如:
volumes:
- name: data-volume
awsElasticBlockStore:
volumeID: <volume-id>
fsType: ext4
Persistent Volumes
在 Pod 文件中直接声明 Volumes 无法支持大集群运维需求。Kubernetes 支持通过配置方式创建大型存储池,让用户根据需要从中划分部分存储。管理员预声明 Persistent Volumes(PVs) 定义存储资源。而用户可以通过 Persistent Volume Claim(PVC) 从 PVs 中选择存储资源。

创建 PV 的配置示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-voll
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 1Gi
hostPath:
path: /tmp/data
Persistent Volume Claims
PV 和 PVC 是 Kubernetes namespace 中的两种独立对象,创建 PVC 后,Kubernetes 将会根据配置将 PV 绑定到 PVC,且每个 PVC 都会绑定到单个 PV。绑定过程中,Kubernetes 将尝试查找满足 PVC 配置条件的 PV,例如是否具有足够容量、访问模式、卷模式、存储类等。PVC 有可能找到多个满足其条件的 PV,可以使用标签(Label)与选择器(Selector)更加精确的绑定对应卷(Volume)。
如果所有条件都匹配,且 PVC 没有更好的选项,那么 PVC 可能会绑定到较大的 PV。PVC 和 PV 之间为一一对应关系,因此在 PVC 绑定较大 PV 的情况出现时,可能会产生卷容量浪费。
当 PVC 无法获取到满足条件的 PV 时,会进入挂起状态,直到有满足条件的 PV 出现时,会自动绑定到新的可用卷。
下面是创建 PVC 示例:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myClaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
persistentVolumeReclaimPolicy: Retain
在删除 PVC 时,可以选择对 PV 执行的操作,默认情况下为 Retain:
- Retain 保留,PV 将一直保留直到被手动删除,也不会再次被 PVC 使用
- Delete 删除,释放 PV 锁定的存储空间
- Recycle 可被其他 PVC 使用,会在绑定前擦除数据
PVC 声明完成后,就可以在 Pod 中使用 PVC 了,示例如下:
apiVersion: v1
kind: Pod
metadata:
name: random-number-generator
spec:
containers:
- image: alpine
name: alpine
command: ["/bin/sh", "-c"]
args: ["shuf -i 0-100 -n 1 >> /opt/number.out;"]
volumeMounts:
- mountPath: /opt
name: data-volume
volumes:
- name: data-volume
persistentVolumeClaim:
claimName: myClaim
Storage Class
存储类的作用是:在应用程序需要时自动配置 PV,示例如下,具体的 parameters 中的参数根据云服务厂商的不同有差异:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: google-storage
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-standard
replication-type: none
配置完成后,就可以不在自己通过 PV 定义创建 PV 了。只需要在 PVC 中声明这个 SC 的名称:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: myClaim
spec:
accessModes:
- ReadWriteOnce
storageClassName: google-storage
resources:
requests:
storage: 500Mi
附加知识点
docker 命令与参数
- CMD: command param1 或 [“command”,”param1″]
- 给出一个容器的默认可执行体,可以覆盖
- ENTRYPOINT: ENTRYPOINT [“command”]
- 入口点命令接收参数,让容器执行表现的像一个可执行程序
CMD 和 ENTRYPOINT 可以组合使用,如果传递参数,则使用参数,如果未传递参数则使用 CMD 中的默认值,如下:
FROM Ubuntu
ENTRYPOINT ["sleep"]
CMD ["5"]
// 对应结果 sleep 10秒
docker run ubuntu 10
// 对应结果 sleep 5秒
docker run ubuntu
// 启动时覆盖 ENTRYPOINT 入口点(不调用 sleep,调用 sleep2)
docker run --entrypoint sleep2 ubuntu 10
Kubernetes 命令与参数
配置 Pod 定义文件的 command 和 args 属性,可以覆盖 ENTRYPOINT 和 CMD
apiVersion: v1
kind: Pod
metadata:
name: pod
spec:
containers:
- name: pod
image: pod-image
command: ["sleep2"] ---> ENTRYPOINT ["sleep2"]
args: ["10"] ---> CMD ["5"]
Kubernetes 中可以通过添加 -- 来传递参数
kubectl run app --image=app-image -- color green
通过 --command 传递 ENTRYPOINT
kubectl run app --image=app-image --command -- python app.py