Kubernetes(7)声明式API

1. 概念

1.1 命令式命令行操作

Docker Swarm的编排操作

$ docker service create --name nginx --replicas 2  nginx
$ docker service update --image nginx:1.7.9 nginx
1.2 命令式配置文件操作

Kubernetes中,通过编写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

创建出来一个Pod之后,更新容器镜像版本:修改yaml文件的Pod template部分

...
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
$ kubectl replace -f nginx.yaml
1.3 声明式API

对于1.2中的yaml文件,进行kubectl apply操作

$ kubectl apply -f nginx.yaml

同样的,修改过镜像版本后,执行kubectl apply,更新pod,触发滚动更新。

$ kubectl apply -f nginx.yaml

kubelet apply和kubelet replace区别

  • replace 是使用新的yaml文件中的API对象,替换原有API对象
  • apply 是对原有API对象PATCH操作
    此外,kubetcl set image, kubectl edit也是对原有API对象的修改
    这就意味着,kube-apiserver在响应式命令请求的时候,一次只能处理一个,否则多个替换操作的结果就可能导致冲突,而对于对于声明式请求来说,一次可以进行多个写操作,具备前面提到过的Merge能力。

所以什么是“声明式API”?
其实就是,通过给一个配置文件(期望的最终状态),然后通过使用支持patch的命令模式,去叠加的演进对象的最终状态的这样一种做事情的方式。

2. 声明式API的在实际项目中的重要意义(Istio)

2.1 Isotio概念

是一个基于Kubernetes项目的微服务治理框架,架构如下所示。


2.2 Envoy容器

从架构图中看出,Istio项目的最根本组件,是运行在每个应用Pod中的Envoy容器。这个应用容器是Lyft公司推出的一个高性能C++网络代理,它以sidecar容器的方式,运行在每一个治理的应用Pod中。他的作用是通过配置Pod中的iptables规则,把整个Pod的进出流量接管下来。
这样,Istio控制层的Pilot组件,就能够通过调用每个Envoy容器的API,对这个Envoy进行配置,实现微服务治理。

什么是微服务治理?

2.3 Enovy的产生与应用场景

举例灰度发布。
仍然以2.1中的Istio架构为例,假设左边的应用是一个正在运行的旧版本应用,右边的应用Pod是这个版本新上线应用。通过Pilot调节两个Pod里Envoy容器的配置,进而调节外部请求到两个Pod的流量比例(比如新版本10%,旧版本应用Pod90%),之后通过逐步过渡比例,完成灰度发布的过程。
在整个微服务治理的过程中,无论是对Envoy的部署,还是对Envoy代理的配置,这些对于用户和应用都是完全“无感”的。
这就有一个问题,既然Envoy是在每个应用Pod安装的(我们知道,可以把一个Pod,理解成一个实际的应用,而Envoy容器的声明和配置修改,都不会在用户的yaml文件中写明,单Envoy容器最终又是切切实实运行在最终的应用Pod当中),那么对于Envoy在Pod中实现和配置修改,Istio怎么做到无感的?
实现这种Envoy容器无感修改的技术,是Kubernetes中一个叫做Dynamic Admission Control的功能,这个功能也叫做Initializer。

后面就可以看到,Dynamic Admission Control,也就是Initilizer,其实也是一个容器起来的Pod。他的功能就是将存在Etcd中的ConfigMap类型的关于Envoy容器的配置,和用户的Pod API对象通过Kubernetes的PATCH API做merge。

实际上,在Kubernetes项目中,每当一个Pod或者任何一个API对象通过命令等方式提交给APIServer之后,总要有一些“初始化”性质的工作需要他们被Kubernetes项目正式处理之前进行。这个操作,是一个叫做Admission的功能。这是Kubernetes中一段Admission Controller的代码,可以被选择性地变异进入API Server,在API对象创建后会被立刻调用。但是,如果Envoy的配置修改想用这个功能的话,那么每次都要动态的修改代码重新build并重启APIServer,显然这是不可接受的。所以就有了“热更新”方式的Admission机制,就是上面提到的动态Admission Control技术(小伙子,该去看代码了= =)

具体到例子来分析,看看Istio的效果。
受限,用户有一个应用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被提交给Kubernetes之后,在对应的API对象中加上Envoy容器的配置(这里就是无感了),使API对象对应的yaml文件如下变成下面这样

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"]
    ...

那么,对这个API对象无感的修改,是怎么通过Initializer来实现的呢?

  • Istio将这个Envoy容器本身的定义,以ConfigMap的方式,存在Etcd当中,这个ConfigMap(叫做envoy-initializer)的定义举例如下:
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的部分就是envoy容器的对应配置字段和volumes的配置。而Initializer的工作,就是把这个部分配置,自动添加到用户的POD API对象中。这就用到了Kubernetes的PATCH API,而这种patch操作,正是声明式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的使用的镜像envoy-initializer:0.0.1就是一个自定义控制器(Custom Controller),是可以自己事先编写好的。
而这个控制器同样也是遵循控制器模型的,他的实际状态是获取到的用户新建的Pod的配置情况,期望状态就是这个Pod里被加入了Envoy容器的定义。伪代码如下:

for {
  // 获取新创建的 Pod
  pod := client.GetLatestPod()
  // Diff 一下,检查是否已经初始化过
  if !isInitialized(pod) {
    // 没有?那就来初始化一下
    doSomething(pod)
  }
}

如果获取到的Pod的API对象中,没有Envoy的容器的定义,那么就开始进行doSomething的初始化,具体做的事情就是前面提到过的,把Etcd中存储的那个关于Envoy容器配置的ConfigMap对象加到一个空的Pod里面去,然后调用Kubernetes的API库中TwoWayMerge
Patch方法,把用户Pod对象和新Pod对象 merge。

  • 至此,Istio对通过声明式API的Patch特性实现Dynamic Adminssion Control功能,进而支持Envoy容器在用户和应用侧无感的能力就实现了。
2.4 Dynamic Admission Control的配置

Dynamic Admission Control,即Initializer的相关配置(注意,这里不是指的Initializer本身,而是Initializer的配置文件)。
我们可以通过配置这个配置对象,来指明对什么类型的资源进行Initialize的操作。比如下面这段代码,就是对全部的pods都做初始化操作。

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

同时,只要这个对象一杯kubectl apply/create出来之后,Kubernetes就会把这个Initializer的名字(envoy.initializer)打在所有新创建Pod的metada上,如下:

apiVersion: v1
kind: Pod
metadata:
  initializers:
    pending:
      - name: envoy.initializer.kubernetes.io
  name: myapp-pod
  labels:
    app: myapp
...

这个字段,就是在Initializer的控制器模型中,根据什么区判断有没有被初始化过的依据,换言之,每当我们自定义的Initialzer在做完Initialize的操作之后,要把metadata.initializers.pending标识删除掉!!!

除了Kubernetes在我们创建出来kind是InitializerConfiguration的对象后,会自动给所有新建的Pod都打这个标签之外,如果我们没有新建这个InitializerConfiguration对象,但是又想让我的某个Pod去使用某个Initializer的话,这么干:给对应Pod打metadata.annotations字段实例如下:

apiVersion: v1
kind: Pod
metadata
  annotations:
    "initializer.kubernetes.io/envoy": "true"
    ...

而Istio项目的核心,就是用无数个运行在应用Pod中的Envoy容器组成的服务代理网格。这也是Service Mesh的含义。

总地来说,从上到下,灰度升级的切流场景->(Service Mesh)->Envoy们组成的服务代理网格->Envoy的实现->Kubernetes通过声明式API支持的Patch修改Pod的能力->声明式API好哇,声明式API棒哇(少林功夫好哇,好!少林功夫棒哇,棒!我系铁头功,无敌铁头功....喂,跑题了,回来了!)

3. 小结

声明式API的独特之处:

  • 声明式API,就是说我们来提供一个好的API对象进行声明,我们期望的状态是啥样子的
  • 其次,声明式API允许多个API对象写端,并在Kubernetes中支持以Patch的方式,对API对象进行修改以达到最终期望的实际状态,而无需关心最初的YAML文件的内容
  • 基于以上两个能力特性(提供API明确的API对象来声明最终的期望状态+支持多个API对象声明并以PATCH的能力使控制器模型拿到的是最终的期望状态),Kubernetes能在无需外部干预的情况下,只要你给我明确的若干个API对象,我就能够给你无感调谐到你最终期望的状态

而在上面我们提到的Initializer的实现时,最核心的,还是Initializer里面那个image,自定义编程的编写过程,这是遵循“Kubernetes范式编程”,即

如何使用控制器模式,同 Kubernetes 里 API 对象的“增、删、改、查”进行协作,进而完成用户业务逻辑的编写过程。

所以,之后要学习的一个核心,就是如何通过“Kuberbetes范式编程”完成使用Kubernetes部署代码的Kubernetes用户,到使用Kubernetes编写代码的Kubernetes玩家的晋级之路。

你可能感兴趣的:(Kubernetes(7)声明式API)