我们分享了很多Kuberenets的API对象,这些API对象,有的用来描述应用,有的为K8S本身提供服务,但是,无一例外,都需要编写一个对应的YAML文件来交给Kubernetes
是不是,使用这个YAML文件,就是声明式API了呢?
举个例子,Docker Swarm的编排操作是基于命令行的
$ docker service create –name nginx –replicas 2 nginx
$ docker service update –image nginx:1.7.9 nginx
那么这两个命令,利用Swarm启动了两个Nginx容器,然后在进行滚动更新了他们
这种使用方式,称为命令式命令行操作
如果是上面的操作在K8S中,应该如何实现呢?
说到底还是需要Deployment来控制管理YAML文件
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
– name: nginx
image: nginx
ports:
– containerPort: 80
然后我们就可以创建这个对象了,
kubectl create -f nginx.yaml
这样,两个Nginx的Pod就会运行了,如果需要修改这个Pod的Nginx,那么怎么办,可以使用kubectl set image和kubectl edit命令,来直接修改kubernetes的API对象
或者直接修改本地文件并且进行重新的创建
我们将接下来的YAML文件中的镜像改为1.7.9
…
spec:
containers:
– name: nginx
image: nginx:1.7.9
然后执行一句kubectl replace 操作,来完成Deployment 的更新
这种基于YAML的操作方式,是声明式API吗,并不是
基于kubectl create kubectl replace的操作,其实本质上应该成为命令式配置文件操作
这种操作的方式,和前面的Docker Swarm的命令,没啥本质上的区别,只不过将命令的参数,写在了配置文件中
那么,什么是声明式API?
kubectl apply,应该算是一个,这个命令是我们推荐使用来替代kubectl create命令的
那么,我们创建了一个Deployment
kubectl apply -f nginx.yaml
然后修改一下nginx.yaml中的定义的镜像
还是可以使用kubectl apply -f nginx.yaml
这样和之前的replace的区别,是在于replace是使用新的YAML文件的API对象进行替换了原有的API对象,而kubectl apply,则是执行了一个对原有API对象的PATCH的操作
进一步的,就意味着kube-apiserver在响应命令式请求的时候,一次只能处理一个写请求,不然会有产生冲突的可能性,对于声明式请求,可以处理多个写操作,并且具有Merge的能力
接下来我们看一下Istio项目中使用的声明式API在实际使用中的重要意义
Istio开源项目的诞生,掀起了一个名为微服务的浪潮,将Service Mesh这个新的编排概念推到风口浪尖
上面是关于Istio的整体架构,Istio最根本的组件,是运行在每一个Pod中的Envoy的容器
在Istio项目,将这个代理服务以sidecar容器的方式,放在了每一个被治理的Pod中,在Pod中,所有的容器都共享一个Network Namespace,所以Envoy容器还能够通过配置Pod的iptabls规则,将整个Pod的进出入流量进行接管
这时候,Istio的控制层里的Pilot组件,可以通过调用每个Envoy容器的API,对这个Envoy代理进行配置,实现微服务治理
假设,这个Istio架构图左边的Pod是已经在运行的应用,右边的Pod是新上线的版本,那么可以通过调节这两个Pod的Envoy容器的配置,先将10%的流量分给新版本,然后逐渐增大
然后,在整个未付治理的过程,无论是对Envoy容器的部署,还是上面的代理,用户和应用都是无感的
那么Istio是如何提前安装Envoy容器,做到无感呢?
Istio项目使用的,是Kubernetes中一个非常重要的功能,叫做Dynamic Admission Control
在Kubernetes项目中,有一个Pod或者任何一个API对象被提交给APIServer之后,都需要做一些初始化的工作,再其正式开始使用前进行,比如,自动为所有的Pod加上某些Labels
这些初始化的操作实现,使用的是一个Admission功能,其实是Kubernetes项目中一组被称为Admission Controller的代码,可以选择性的被编译到ApiServer中,在API对象被创建后会立刻被调用到
但是,这样,说明,如果需要自己添加一些新的规则进入Admission Controller的话,会比较困难,需要重新编译并且重启APIServer,那么,Kubernetes提供了一种热插拔的Admission的机制,就是Dynamic Admission Control,或称Initializer
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’]
比如,我们就是要在这个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”]
…
那么,是如何额外在初始化的时候,定义一个envoy的容器呢?
Istio是如何在用户完全不知情的前提下注入的呢?
Istio会将Envoy容器本身的定义,以configMap的方式保存在Kuberentes当中,例如下面
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对象的定义,
那么Initializer要做的工作,就是讲这部分Envoy相关的字段,添加到用户提交的Pod的API对象,用户提交的Pod的本来就有containers字段和volumes字段,所以需要考虑要有类似git merge这样的操作,将两部分合并在一起
Initializer在更新用户的Pod对象时候,必须要使用PATCH API来完成,这种PATCH API,正是声明式API的主要能力
接下来,Istio将一个编写好的Initialzer,作为一个Pod部署在Kubernetes中,这个Pod的定义很简单,
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使用的镜像,是一个定义好的自定义控制器,这个控制器的功能和标准控制器类似
就是一个死循环,不断的获取实际状态,和期望状态进行对比,决定下一步的操作
Initializer的控制器,就是不断的获取到实际状态(用户自定义的Pod),和期望状态(是否添加了Envoy容器)进行对比
那么这一步的PATCH操作,利用的就是envoy-initializer中的ConfigMap中的数据
那如何判断是否需要进行PATCH操作呢?
我们先进行一个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
这个配置,意味着Kubernetes要对所有的Pod进行匹配,而且指定了负责的操作的Initializer,名为envoy-initializer
这个InitializerConfiguration被应用,那所有新创建的Pod的Metadata字段,会被加上这个Initializer的名字
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做完了要做的操作后,要将这个pending的标志清除掉,这一点,要在编写Initializer代码要非常的注意
除了上面的配置方式,还可以再具体的Pod的Annotation中添加一个表明自己要用什么Initializer的字段
比如下面
apiVersion: v1
kind: Pod
metadata
annotations:
“initializer.kubernetes.io/envoy”: “true”
…
就是在annotations中声明使用了initializer.kubernetes.io/envoy的initializer
而在Patch的过程中
我们需要先从ConfigMap中拿到这个Pod的定义,然后将这个ConfigMap中存储的containers和volumes字段添加到一个新的Pod对象中
然后我们利用Kubernetes的API苦,来讲两个Pod进行合并,生成一个TwoWayMergePatch,有了这个Patch,就可以调用kubernetes的Client发起一个PATCH请求,这样这个用户提交的Pod,就会被加上Envoy的字段
这样,就是Istio项目的组成,由无数个运行在Pod中Envoy容器组成的服务代理网格,也是Service Mesh的定义
而这个操作的实现原理,就是Kubernetes声明式API的独特之处
我们可以提交一个定义好的API来声明,期望状态
而这个API,可以有多个写段,以PATCH的方式对API对象进行修改,无需关心本地的YAML文件的内容
最重要的是,有了上面的能力,可以进行API的增删改查
这也是Kubernetes项目编排能力赖以生存的核心所在
而整个Istio项目部署完成,大约会创建43个API对象,而整体部署,都依赖了Kubernetes的声明式API
而Initializer的使用过程汇总,主要是对于Initializer自定义控制器的编写过程,遵循的正是Kuberenetes的编程范式,即如何使用控制器模式,通过Kubernetes中API对象的增删改查进行写作,完成用户的业务逻辑
本章中,我们重点讲解了Kubernetes声明式API的问题,并通过对Istio项目的解析,说明了Kuberenetes中的Initializer的特性,完成了Envoy容器自动注入的原理
这样,就是从Kubernetes部署代码,到使用Kuberenets编写代码的一个蜕变之路
那么,本章思考一下,为何Envoy能击败Nginx和HAProxy,称为Service Mesh体系的核心
因为基于声明式API吧,暴露API方便二次开发和调试