声明式API
kubectl apply就是声明式,只需要描述结果,并不关心如何做。这个命令既可以用来启动Pod,也可以用来更新Pod。
对于更新也可以使用kubectl replace,但是它是响应式命令操作,两者的本质不同在于kubectl replace 的执行过程,是使用新的 YAML 文件中的 API 对象,替换原有的 API 对象;而 kubectl apply,则是执行了一个对原有 API 对象的 PATCH 操作。
kube-apiserver 在响应命令式请求(比如,kubectl replace)的时候,一次只能处理一个写请求,否则会有产生冲突的可能。而对于声明式请求(比如,kubectl apply),一次能处理多个写操作,并且具备 Merge 能力。
以Istio为例,来说明声明式API在实际使用中的重要意义。Istio的架构图为:
Istio 最根本的组件,是运行在每一个应用 Pod 里的 Envoy 容器。这是Lyft公司推出的一个高性能C++网络代理。Istio将这个代理服务以sidecar的方式运行在每一个被治理的Pod中。每个Pod的所有容器都共享一个Network Namespace,所以这个Envoy就可以通过配置Pod里的iptables规则来接管整个Pod的进出流量。
Istio 的控制层(Control Plane)里的 Pilot 组件,就能够通过调用每个 Envoy 容器的 API,对这个 Envoy 代理进行配置,从而实现微服务治理。
在整个微服务治理的过程中,无论是Envoy容器的部署还是对Envoy代理的配置,用户和应用都是无感的。那么就有了一个问题。
Istio在每一个治理的Pod中都安装了Envoy容器,是如何无感的?
答案就是Istio使用了k8s的一个重要功能,Dynamic Admission Control。
k8s中存在一组被称为Admission Controller的可以被编译进APIServer的代码,用来在一个Pod或者API对象被提交给APIServer被k8s处理之前进行一个初始化工作,例如给Pod加上某些标签。如果想要添加自己的规则到Admission Controller需要重新编译启动APIServer,这是不能够接受的,因此k8s提供了一种“热拔插”式的Admission机制,就是Dynamic Admission Control。也叫做Initializer。
Initializer
例如,有如下的应用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被提交给k8s之后,在其对应的Pod YAML中加上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完成这个功能通过以下步骤:
- 将Envoy这个容器本身的定义以ConfigMap的方式保存在k8s中。ConfigMap的定义如下:
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可以看到data字段里就是一个Pod对象定义的一部分。所以需要把这部分与用户提交的Pod的字段进行合并。也就是更新用户Pod,必须使用PATCH API来完成。这种PATCH API正是声明式API最主要的能力。
- 将一个编写好的Initializer作为一个Pod部署在k8s中,其YAML如下:
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就是一个事先编写好的自定义控制器。这个控制器负责的任务就是不断检查用户新创建的Pod,然后如果没有添加过Envoy容器就修改这个Pod的API对象,即将前面创建的ConfigMap中的字段合并到这个Pod的YAML中。
k8s提供了合并Pod的方法CreateTwoWayMergePatch:
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)
}合并之后发起PATCH请求,这样一个用户的Pod就会自动被加上Envoy的容器字段。
k8s还可以通过配置文件来指定对什么样的操作进行这个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表示对所有的Pod都使用evnoy-initializer进行Initialize操作。一旦创建了这个InitializerConfiguration,那么所有新建的Pod的Metadata都会加上这个Initializer的名字:
apiVersion: v1
kind: Pod
metadata:
initializers:
pending:
- name: envoy.initializer.kubernetes.io
name: myapp-pod
labels:
app: myapp
...这个 Metadata,正是接下来 Initializer 的控制器判断这个 Pod 有没有执行过自己所负责的初始化操作的重要依据。当你在 Initializer 里完成了要做的操作后,一定要记得将这个 metadata.initializers.pending 标志清除掉。
还可以在具体的Pod的Annotation里添加如下字段,声明这个Pod中要使用某个Initializer:
apiVersion: v1
kind: Pod
metadata
annotations:
"initializer.kubernetes.io/envoy": "true"
...小结
- 首先,所谓“声明式”,指的就是我只需要提交一个定义好的 API 对象来“声明”,我所期望的状态是什么样子。
- 其次,“声明式 API”允许有多个 API 写端,以 PATCH 的方式对 API 对象进行修改,而无需关心本地原始 YAML 文件的内容。
- 最后,也是最重要的,有了上述两个能力,Kubernetes 项目才可以基于对 API 对象的增、删、改、查,在完全无需外界干预的情况下,完成对“实际状态”和“期望状态”的调谐(Reconcile)过程。
声明式 API,才是 Kubernetes 项目编排能力“赖以生存”的核心所在。