声明式API与Kubernetes编程范式
Istio项目
Dynamic Admission Control
在 Kubernetes 项目中,当一个 Pod 或者任何一个 API 对象被提交给 APIServer 之后,在K8S项目正式处理之前会通过 项目里一组被称为 Admission Controller 的代码实现 Admission 的功能 来 进行一些初始化工作(如自动为所有 Pod 加上某些标签(Labels)。)
如果想要添加一些自己的规则到 Admission Controller(需要重新编译并重启 APIServer)。
所以,k8s提供了一种**“热插拔”**式的 Admission 机制,它就是 Dynamic Admission Control,也叫作:Initializer。
用户提交的Pod中只有一个容器:myapp-pod
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
Istio 项目要做的,就是在这个 Pod YAML 被提交给 Kubernetes 之后,在它对应的 API 对象里自动加上 Envoy 容器的配置,使这个对象变成如下所示的样子:
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
spec:
containers:
- name: myapp-container
image: busybox
command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']
- name: envoy
image: lyft/envoy:845747b88f102c0fd262ab234308e9e22f693a1
command: ["/usr/local/bin/envoy"]
...
如何做到呢
Istio 编写一个用来为 Pod“自动注入”Envoy 容器的 Initializer。
Istio 会将这个 Envoy 容器本身的定义,以 ConfigMap 的方式保存在 Kubernetes 当中。
apiVersion: v1
kind: ConfigMap
metadata:
name: envoy-initializer
data:
config: |
containers:
- name: envoy
image: lyft/envoy:845747db88f102c0fd262ab234308e9e22f693a1
command: ["/usr/local/bin/envoy"]
args:
- "--concurrency 4"
- "--config-path /etc/envoy/envoy.json"
- "--mode serve"
ports:
- containerPort: 80
protocol: TCP
resources:
limits:
cpu: "1000m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "64Mi"
volumeMounts:
- name: envoy-conf
mountPath: /etc/envoy
volumes:
- name: envoy-conf
configMap:
name: envoy
Initializer 需要把envoy的相关字段merge到用户提交的Pod内容中,(使用PATCH API实现,时声明式API的主要能力)
Istio 将一个编写好的 Initializer,作为一个 Pod 部署在 Kubernetes 中。
apiVersion: v1
kind: Pod
metadata:
labels:
app: envoy-initializer
name: envoy-initializer
spec:
containers:
- name: envoy-initializer
image: envoy-initializer:0.0.1
imagePullPolicy: Always
envoy-initializer:0.0.1 镜像,就是一个事先编写好的“自定义控制器”(Custom Controller)
功能:
for {
// 获取新创建的 Pod
pod := client.GetLatestPod()
// Diff 一下,检查是否已经初始化过
if !isInitialized(pod) {
// 没有?那就来初始化一下
doSomething(pod)
}
}
//在 Initializer 控制器的工作逻辑里,它首先会从 APIServer 中拿到这个 ConfigMap
//把这个 ConfigMap 里存储的 containers 和 volumes 字段,直接添加进一个空的 Pod 对象里:
//Kubernetes 的 API 库,为我们提供了一个方法,使得我们可以直接使用新旧两个 Pod 对象,生成一个 TwoWayMergePatch
func doSomething(pod) {
cm := client.Get(ConfigMap, "envoy-initializer")
newPod := Pod{}
newPod.Spec.Containers = cm.Containers
newPod.Spec.Volumes = cm.Volumes
// 生成 patch 数据
patchBytes := strategicpatch.CreateTwoWayMergePatch(pod, newPod)
// 发起 PATCH 请求,修改这个 pod 对象
client.Patch(pod.Name, patchBytes)
}
Kubernetes 还允许你通过配置,来指定要对什么样的资源进行这个 Initialize 操作
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
name: envoy-config
initializers:
// 这个名字必须至少包括两个 "."
- name: envoy.initializer.kubernetes.io
rules:
- apiGroups:
- "" // 前面说过, "" 就是 core API Group 的意思
apiVersions:
- v1
resources:
- pods
一旦这个 InitializerConfiguration 被创建,Kubernetes 就会把这个 Initializer 的名字,加在所有新创建的 Pod 的 Metadata 上
apiVersion: v1
kind: Pod
metadata:
initializers:
pending:
- name: envoy.initializer.kubernetes.io
name: myapp-pod
labels:
app: myapp
...
每一个新创建的 Pod,都会自动携带了 metadata.initializers.pending 的 Metadata 信息
这个 Metadata,正是接下来 Initializer 的控制器判断这个 Pod 有没有执行过自己所负责的初始化操作的重要依据
当在 Initializer 里完成了要做的操作后,一定要metadata.initializers.pending 标志清除掉。
声明式API的工作原理
总结
K8s中,一个API对象在etcd中完整的路径是由Group(API组)、Version(API版本)、Resource(API资源类型)3个部分决定的。
核心 API 对象
对于 Kubernetes 里的核心 API 对象,比如:Pod、Node 等,是不需要 Group 的(即:它们