为了让读者能更容易理解Kubernetes提供的安全机制,笔者决定从安全的视角,重新介绍一次Kubernetes的体系架构,以期能够在介绍如何创建secrets对象的时候,大家非常清楚kubectl apply命名执行后,Kubernetes的核心组件是如何工作,以及对整个Kubernetes的体系架构有更加全面的认知。
虽然说笔者在前边的多篇文章中已经反复,详细的介绍了Kubernetes的架构体系,但是并不妨碍这篇文章继续唠叨,就如同我说的,咱从安全的视角来再次温习一下。Kuberntes的体系架构从宏观角度来看,主要分为Control Panel和Worker Panel组件,有时候我们也称作是master node和worker node。在典型的Kubernetes集群中(无论是单机集群,多机集群还是托管集群ACK),我们总是能找到这两类组件。
如果我们从开发运维人员的角度看,Kuberntes集群的体系架构如下图所示。工作节点(worker node)负责运行开发和运维同学提交的工作任务(比如帮我运行3个应用程序实例,运行一个MYSQL数据库实例,或者启动一个负载均衡服务器等);管理节点(master node)主要负责管理工作节点,并且分配计算资源给我们提交的任务(也叫调度)。
如果读者想本地启动并运行一个Kubernetes集群,最小的资源配置是必须有一台node,这台node同时扮演了管理节点和工作节点,比如我们在系列文章中一直使用的minikube集群。如果你要搭建生产环境,笔者强烈建议选择阿里巴巴的ACK托管服务,自己搭建多机集群需要较高的运维水平和长期的人力资源投入,才可能实现。
如上图所示,整个K8S平台由Control panel和Worker panel组成,虽然说图中并没有细化每个部分内部的细节,但是我相信笔者应该能意识到,这是一张简化的图,每个panel中又包含更多的组件,接下来我们就跟进一步,看看组成Kubernetes平台的两个panel中具体有哪些component。
我们从Mater node开始,Mater node人如其名啊,是整个集群的大脑,负责管理整个集群,比如决定提交的任务应该在哪个工作节点上运行,如果应用运行过程中出现故障,如何处理这种异常等等。Master node有一个非常非常重要的职责,就是整个集群的入口,开发人员和运维人员,以及集成工具作为整个集群的用户,需要通过Master node来访问集群提供的功能。
具体来说,Master node提供的功能主要由以下四个子组件组成:
- kube-apiserver,开发人员和运维人员使用Kubernetes集群功能的入口,服务能力通过restful风格的API对外暴露。对于运维人员来说,当我们在客户端运行kubectl apply的时候,背后调用的就是apiserver提供的服务接口。
- etcd,高性能的内存型键值对数据库,用来持久化集群上所有需要落盘保存的数据。当我们通过kubectl apply向集群提交任务后,YAML文件会被保存到etcd中;当我们通过对象的资源URL获取对象YAML信息的时候,数据从etcd中读出。
- scheduler,当用户提交任务后,在Master node上,scheduler组件负责基于用户的资源请求,来决定任务在哪个工作节点运行。”决定工作节点“的过程我们一般称之为”调度“,而调度一般需要参考很多因素,比如节点上的当前负载,资源配置情况,硬件信息等等。
- controller,控制器的职责是监控kubernetes的对象实例,每个控制前controller负责监控一种类型的对象。目前Kubernetes集群上有四种主要的控制器:1,node控制,用来监控节点的状态,当节点故障后,会执行必要的功能;2,replication控制器,主要用来监控和维护应用程序的正常运行;3,endpoint控制器,主要用来让应用程序可以通过静态IP和DNS名称被访问;4,service account和token类型的控制器,主要用来为新的命名空间创建默认的账户和token令牌。
管理节点上的四个组件,以及关系如下图所示,
了解了管理节点后,接下来我们来深入看看工作节点上具体有哪些组件。如前文所述,工作节点上运行的是我们提交的任务,由于在Kubernetes平台,任务总是以容器的方式运行,因此不难理解每个工作节点上有有容器组件,比如Podman或者Docker。基于上边的讨论,工作节点主要由如下三个组件组成:
- kubelet,可以看成以运行在工作节点上的代理,主要负责和Master node通信,接受由分配的任务,以及驱动容器运行时将任务在工作节点上运行起来。
- proxy,网络访问代理,用来把工作节点上部署的应用程序实例(POD)提供的拂去暴露给外部访问。
- container runtime,容器运行时组件,负责以特定容器的方式来启动应用程序。Kunerntes支持的容器运行时包括但不限于:containerd,docker和CRIO-O等(严格说实现了CRI规范的容器运行时都可以集成到Kubernetes平台上)。
工作节点上的三个组件,以及关系如下图所示,
了解完了Kubernetes平台体系结构之后,对于技术开发人员,其实最关心的还是如何将我们自己的应用程序部署到K8S平台上,以及应用程序提供的服务能够从集群外部被访问到,这是我们接下来要介绍的内容。
在Kuberntes平台上,资源的最小调度单位是POD,因此我们部署应用程序,就是就是将应用的可运行包部署到一个一个POD中。当然POD中可以有多个应用程序(容器实例),但是同一个POD中的多个容器实例共享IP,网络资源,生命周期等。至于两个应用程序应该被部署到同一个POD还不同的POD,这取决于两个应用程序的亲密性,笔者有一个最简单的判断方法:如果两个应用有相同的生命周期,并且通过localhost,命名管道等机制来进行数据交互,那么就应该被部署到同一个POD中,不过大家要注意,大部分应用程序应该被部署到不同的POD中,只有在特殊情况下才选择部署到同一个POD中这种方式。
POD可以被类比为传统部署方案中的虚拟机(VM),我们可以在虚拟机上运行多个进程(容器进程),这些进程共享虚拟机的硬件资源(POD的资源申请),以及同一台虚拟机上的多个进程有相同的生命周期(POD中的多个容器进程有相同的生命周期),这两个抽象模型的也有很多区别,比如POD是个逻辑的概念,我们在POD中运行的是容器,一种特殊的进程(特殊性的理解可以参考笔者前边的多篇文章)等等。
在Kubernetes集群上部署应用程序有很多种方式,但是最最常用的是通过kubectl客户端命令行工具。首先我们需要创建如下图所示的YAML文件,YAML文件可以理解为声明式的应用程序部署架构:
有了这个POD的YAML文件定义,接下来我们通过kubectl apply -f hello-pod.yaml来将应用程序部署到Kubernetes集群。如前边多篇文章所属,我们可以通过kubectl get pods来查看pod的运行状态,我们可以通过kubectl describe pod yunpan来获取这个pod的详细信息,我们可以通过kubectl logs来获取运行日志,我们还可以通过kubectl delete pod yunpan来彻底删除这个应用程序的实例。
虽然说通过kubectl提供的这些操作pod的命令可以做很多事情,但是我相信如果你有复杂系统的部署和运维经验的话,你肯定会说”这有啥用!“。没错,操作单个POD没有太大的意思,因为三高系统基本都会部署多个应用的实例来组成集群对外提供服务。并且当某个实例挂掉后,系统会检测到这个错误,并重新启动实例,来提供稳定的服务给客户端。
在Kubernetes平台上,如果我们需要如上描述的健壮性,就需要创建Replicaset对象,来管理我们的应用程序实例。但是Replica Set通常不是手动创建,而是通过Deployment对象,一个Deployment对象总是会创建一个Replica Set对象与之关联。因此当我们将服务作为Deployment对象部署的时候,在背后会创建一个Replica Set对象来监控应用程序的运行实例,当有实例因为某种原因挂掉后,Replica Set会重新实例,如下图所示:
接下来我们来创建一个Deployment部署YAML文件,和POD的YAML文件比起来,Deployment对象包含的字段更多,比如通过replicas来设置期望的应用实例数量,另外由于Deployment部署后需要启动应用程序的实例,因此也需要配置应用程序的镜像,端口号等。如下图所示:
有了应用的YAML定义,我们赶紧通过kubectl apply -f来部署到自己的本地集群中。部署完成可以通过kubectl get pods来查看应用的运行状态。从输出你会看到pod实例的名称后边被追加了”一串字符“,比如在笔者的minikube环境上,最终生成的POD名称为:hello-deployment-545574bbc6-kq24g,而我们在YAML文件中并不是这样设置的啊,关于这边我们后边再说。
因为对于Kubernetes来说,Deployment也是一种对象,因此我们也可以通过kubectl get deployments来查看deployment对象的信息,在笔者的本地环境中,输出如下:
➜ Kubernetes安全 kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
hello-deployment 1/1 1 1 5m
这里我们只需要关心的是avaliable列,显示我们的应用程序已经可用(available),由于咱的Deployment文件中,tempalte部分只定义了一个容器,因此这里的”1“代表的就是我们的springcloud应用程序k8ssample。接下里,我们来做个破坏性的实验,把名叫”hello-deployment-545574bbc6-kq24g“这个pod给删除了,看看会发生什么。在自己的本地环境中执行kubectl delete pod hello-deployment-545574bbc6-kq24g(注意替换为自己环境的pod名称),然后过几秒钟后再次执行kubectl get pods,你会发现有出现了一个类似的POD(除了POD名称中那串奇怪的字符不一样之外)。在笔者的本地环境中,新的pod叫”hello-deployment-545574bbc6-494kh“,你可能会有疑问,到底发生了什么?
笔者在执行上边操作的时候,新开了一个窗口,并在里边运行了kubectl get events -w命令来实时的观察整个集群的事件信息(如果对这个原理不清楚,可以仔细阅读笔者的《配置管理系列》文章),输出如下图所示:
从事件信息的输出中,我们可以看到当POD被删除后,马上就会重新创建一个出来,这就是Replica Set的功劳,也就是我们在前文中一直强调的自动重启功能,这是Deployment部署提供高可用的基石。对于开发和运维人员来说,我们终于不用自己手动去重启应用了。
应用程序被成功部署,并且我们Deployment创建的Replica Set为我们的应用程序提供的高可用保障,那么如何访问我们的应用程序呢?特别是考虑到每个POD都有自己独有的IP地址,如果我们在Deployment中将replicas设置为2,那么应用其实有两个实例在同时运行,如果我们要访问,具体应该访问哪一个呢?即便是我们确定了某个实例,但是由于POD可能会被重启,重启后IP地址会发生变化,因此这不是一种具备生产条件的访问方式。
对于这个问题,Kubernetes提供了Service对象来将一组POD代理给外部的用户访问,Service对象有稳定的DNS名称和IP地址,并且对代理的多个POD提供了负载均衡。接着我们来继续创建Service对象,来将我们的pod暴露给外部用户访问。另外笔者已经将本地环境的POD数量调整为2。
我们的目的就是创建一个Service对象来将访问流量负载到这两个实例上。在创建具体Service的YAML文件之前,我们需要先了解一下label(标签)机制在Kubernetes平台上的作用。对于任何Kubernetes平台上的资源来说,我们可以在创建对象实例的时候,为它们指定一个或者多个label(键值对),这样使用者就可以通过键值对来找到”这些“对象。
如果我们仔细看图1.6定义的deployment对象,你会发现我们为POD定义了简key为app,以及值value为yunpan-demo的标签,这样的话,我们在定义Service对象的时候,就可以通过这个label(标签)来找到所有需要被代理的POD对象实例。有了label的概念之后,我们就可以定义Service对象了,如下图所示:
如上图所示,我们在hello-service这个文件中,通过label定义了这个service要代理的pod组,以及端口映射信息。有了YAML文件之后,废话不多说了,直接将Service对象部署到我们的本地minikube集群中。由于我们使用的是minikube,集群中没有负载均衡组件,因此你会看到这个service没有external ip,使用托管Kubernetes环境的同学会不太一样,我们这里的讨论以minikube环境为准。运行命令kubectl get services的输出如下:
➜ Kubernetes安全 kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hello-service LoadBalancer 10.108.230.86 <pending> 8080:31531/TCP 7s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 291d
笔者在前边的文章中曾经介绍过三种在minikue环境中访问Service代理的服务能力的方式,感兴趣的同学可以去看看。在自己的环境中运行minikube service hello-service --url来获取本地的服务访问代理,在笔者的机器上是http://127.0.0.1:65452,接下来我们就可以用这个URL来访问hello-service代理的两个POD的服务,如下图所示:
从上图可以看到,Servie充当我们两个POD的负载均衡器,我们的请求会被均衡的负载到两个实例上,从输出的结果就能看出。
有了上边的知识储备,接下来我们可以来看看如何把隐私数据,比如密码,API keys,以及证书传递给运行的应用程序实例。Secrets和configMap基本类似,主要用来承载键值对类型的配置信息,我们可以把Secret对象中的键值对信息:以环境变量的形式注入容器进程或者以文件的形式挂载到容器的文件系统。
笔者在前边的文章中强调过,在Secrets对象中,数据以base64编码的形式被保存(在etcd中也是以base64编码的形式),而base64编码是一种编码格式,把我们的字符串信息转换成另外一种形式,比如字符串”qigaopan“通过base64编码,结果就是”cWlnYW9wYW4=“。大家需要注意的是,base64编码并不等同于加密,因为base64是很容易被还原成明文。
接下来我们来创建一个Secret类型的对象,Secret提供了两种方式来设置数据:data和stringData。stringData用来设置原始数据,当数据被保存到Secret对象后,会被自动进行base64编码;而data用来设置我们自己手动base64编码后的数据。如下图所示,我们通过stringData来设置字符串:
有了Secret对象的定义,使用熟悉的kubectl工具来把Secret对象部署到本地集群中。接着我们需要修改POD的定义,将secret对象中的数据注入到容器的环境变量中,具体的YAML文件定义如下:
待Deployment运行起来之后,在自己的机器上运行curl命令来访问服务,我们就会看到数据可以被读取出来,如下是在笔者机器上的输出:
➜ Kubernetes安全 kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
hello-deployment 2/2 2 2 50s
➜ Kubernetes安全 curl http://127.0.0.1:51520/v1/yunpan/k8s/hello
被访问机器的Host:hello-deployment-7cfcbf7898-sb6cq ,IP地址是:172.17.0.8 ,环境变量YUNPAN_NAME:QI GAOPAN%
从输出的结果可以看到,我们成功的从环境变量中读取到了在Secret对象中配置的信息。这里要提醒大家的是,千万不要在生产环境这么干,因为这种方式注入到环境变量后,任何运行在POD中的容器实例,都可以访问到这个数据,笔者这里只是为了展示之用。
接下来我们来看看如何将Secret对象以文件的形式挂载到容器的文件系统中,如下图所示:
在环境中部署新版本后,我们在可以深入到容器中,看看是否被正确的挂载,如下是笔者机器上的输出:
➜ Kubernetes安全 kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-deployment-6986665c9-8sp9x 1/1 Running 0 3m50s
hello-deployment-6986665c9-qpbth 1/1 Running 0 3m50s
➜ Kubernetes安全 kubectl exec hello-deployment-6986665c9-8sp9x -- ls /etc/sec
yunpan.name
➜ Kubernetes安全 kubectl exec hello-deployment-6986665c9-8sp9x -- cat /etc/sec/yunpan.name
QI GAOPAN%
从如上的输入可以看到,我们的Secret对象被以文件名yunpan.name挂载到了容器的文件系统中。读者可能会有个疑问,如果Secret中的数据仅仅是base64编码,那么安全体现在哪里呢?或者说为什么Kubernetes会把这个对象类型叫secret?
从使用场景的角度,Secret只有真正被Pod使用的时候,才会被挂载到POD的文件系统,并且最为重要的是,虽然说我们将secret以文件的形式挂载到了文件系统中,其实在容器内,这个节点的文件是tmpfs文件系统,我们知道这个类型的文件系统将数据保存在内存中,因此其实没有将数据写到磁盘上。
当包含这个tmfs文件系统的POD被删除后,这些数据也会从内存中被回收,这其实就是Kubernetes中为啥叫这个对象为secret的原因。Secret保存的数据,其实我们也叫应用的配置信息,这些数据一般都以文件信使存在,在应用启动的时候被加载到应用程序中。对于Secret中保存的信息,我们知道是以base64编码的形式在etcd中保存,因此为了提升安全性,我们必须将数据进行加密,或者叫encryption at rest。
Kuberntes支持将数据进行加密保存在etcd持久化数据库中,这样我们就获得了另外一个层次的安全保障。
好了, 今天这篇文章就到此为止了,在这里笔者想问读者一个问题,你觉得上边的这种将Secret数据注入和挂载到容器中的方式是否安全?或者你有哪些手段可以获取到yunpan.name这个键值对的数据?如果不安全,我们应该怎么做?
笔者会在下篇文章中首先回答上边的三个问题,然后介绍什么才是安全的方式,敬请期待!