极客时间-47
简介
安全问题的唯一正解在于允许那些(导致安全问题的)Bug发生,但通过额外的隔离层来阻拦住它们。
——LinuxCon NA 2015, Linus Torvalds
不论是gVisor还是Kata Container, 其实现的本质,是给进程分配一个独立的操作系统内核,从而避免让容器共享宿主机的内核。这样,容器进程能看到的攻击面,就从整个宿主机内核变成了一个极小的、独立的、以容器为单位的内核。从而有效解决了容器进程发生“逃逸”或者夺取整个宿主机的控制权的问题。
Kata Container和gVisor的区别在于,Kata Container使用的是传统的虚拟化技术,通过虚拟硬件模拟出了一台“小虚拟机”,然后再这个小虚拟机里安装了一个裁剪后的Linux内核来实现强隔离。而gVisor的做法更加激进,Google的工程师直接用Go语言“模拟”了一个运行在用户态的操作系统内核,然后通过这个模拟的内核来代替容器进程向宿主机发起有限的、可控的系统调用。
详细解读kata containers
Kata Containers的工作原理可以用如下所示的示意图来描述:
Kata Containers的本质就是一个轻量化虚拟机。当启动一个Kata Containers之后,会看到一个正常的虚拟机在运行。其意味着,一个标准的虚拟机管理程序(Virtual Machine Manager, VMM)是运行Kata Containers必备的一个组件。在上图中,使用的VMM就是Qemu。
使用了虚拟机作为进程的隔离环境之后,Kata Containers原生就带有了Pod的概念。即:这个Kata Containers启动的虚拟机,就是一个Pod;而用户定义的容器,就是运行在这个轻量级虚拟机里的进程。在具体实现上,Kata Containers的虚拟机里会有一个特殊的Init进程负责管理虚拟机里面的用户容器,并只为这些容器开启Mount Namespace。所以这些用户容器之间,原生就是共享Network以及其他Namespace。
此外,为了跟上层编排框架比如Kubernetes进行对接,Kata Container项目会启动一系列跟用户容器对应的shim进程,来负责操作这些用户容器的生命周期。当然,这些操作,实际上还是靠虚拟机里的Init进程来完成。Kata Containers默认使用的VMM是Qemu。
gVisor设计原理
gVisor工作的核心,在于它为应用进程、也就是用户容器,启动了一个名叫Sentry的进程。而Sentry进程的主要职责,就是提供一个传统的操作系统内核的能力,即:运行用户程序,执行系统调用。所以说,Sentry并不是使用Go语言重新实现一个完整的Linux内核,而只是一个对应用进程“冒充”内核的系统组件。
CNCF补充
基础术语
OCI(开放容器标准),规定了2点:
- 容器镜像要长什么样,即ImageSpec,里面大致规定就是你这个东西需要的是一个压缩的文件夹,文件夹里以xxx结构放xxx文件;
- 容器需要接收哪些指令,这些指令的行为是什么么,即RuntimeSpec。这里面的大致内容是是“容器”要能够执行"create", “start”, “stop”, "delete"命令,并且行为要规范。(OCI的runtime spec对于容器的状态描述,以及对于容器的创建、删除、查看等操作进行了定义)
runc,是对于OCI标准的一个参考实现,是一个可以用于创建和运行容器的CLI(command-line interface)工具,runc直接与容器所依赖的cgroup/linux kernel等进行交互,负责为容器配置cgroup/namespace等启动容器所需的环境,创建启动容器的相关进程。
为了兼容OCI标准,docker也做了架构调整,将容器运行时相关的程序从docker daemon剥离出来,形成了containerd。Containerd向docker提供运行容器的API,二者通过grpc进行交互,containerd最后会通过runc来实际运行容器。
kubernetes在初期版本中,就对多个容器引擎做了兼容,因此可以使用docker、rkt对容器进行管理,以docker为例,kubelet会启动一个docker manager,通过直接调用docker的api进行容器的创建等操作。
在k8s 1.5版本之后,k8s推出了自己的运行时接口api—CRI(container runtime interface)。cri接口的推出,隔离了各个容器引擎之间的差异,而通过统一的接口与各个容器引擎之间进行互动。与oci不同, cri与k8s的概念更加贴合,并紧密绑定,cri不仅定义了容器的生命周期管理,还引入了k8s中pod的概念,并定义了管理pod的生命周期。在k8s中,pod是由一组进行了资源限制,在隔离环境中的容器组成。而这个隔离环境,称之为PodSandbox。在cri开始之处,主要是支持docker和rkt两种,其中kubelet是通过cri接口,调用docker-shim,并进一步调用docker api实现的。docker独立出来containerd,kubernetes也顺应潮流,孵化了cri-containerd项目,用以将containerd接入到cri的标准中。
为了进一步与oci兼容,kubernetes还孵化了cri-o,成为了架设在cri和oci之间的一座桥梁。通过这种方式,可以方便更多符合oci标准的容器运行时,接入kubernetes进行集成使用。可以预见到,通过cri-o,k8s在使用的兼容性和广泛性上将会得到进一步的加强。
Kata Containers: 云原生化的虚拟化
具体过程为:
- 当containerd拿到一个请求的时候,它会首先创建一个shim-v2,这个shim-v2就是一个PodSandbox的代表,也就是那个VMM的代表;
- 每个Pod都会有一个shim-v2来为containerd/CRI-O来执行各种各样的操作。shim-v2会为这个Pod启动一个虚拟机,在里面运行着一个linux kernel,也就是图里面的guest kernel。如果这个里面用的是qemu,会通过一些配置和一些补丁,让其变得更小,同时这里面没有额外的Guest操作系统,不会跑一个完整的像Centos,Ubuntu这样的操作系统;
- 之后会把这个容器的spec以及这个容器本身打包起来的存储,包括rootfs和文件系统,交给这个PodSandbox。这个PodSandbox会在虚拟机中由kata-agent把这个容器启动起来;
- 依照CRI语义和OCI规范,在一个Pod里面可以启动多个相关联的容器,它们会被放在同一个虚拟机里面,并且可以依据需求共享某些namespace;
- 除了这些之外,其他的一些外置的存储和卷也可以通过热插拔的方式来插到这个PodSandbox里面;
- 对于网络,目前使用tcfilter就可以无缝地接入几乎所有的k8s的CNI插件,而且还提供了一个enlightened的模式,这样会有一个特制的CNI插件来提供容器的网络能力。
Github: Documentation
https://github.com/kata-containers/documentation/blob/master/design/architecture.md
The container process is then spawned by agent, an agent process running as a daemon inside the virtual machine. kata-agent
runs a gRPC server in the guest using a VIRTIO serial or VSOCK interface which QEMU exposes as a socket file on the host. kata-runtime
uses a gRPC protocol to communicate with the agent. This protocol allows the runtime to send container management commands to the agent. The protocol is also used to carry the I/O streams(stdout, stderr, stdin) between the containers and the manage engines. (e.g. Docker Engine).
For any given container, both the init process and all potentially executed commands within that container, together with their related I/O streams, need to go through the VIRTIO serial or VSOCK interface exported by QEMU. In the VIRTIO serial case, a Kata Container proxy (kata-proxy
) instance is launched for each virtual machine to handle multiplexing and demultiplexing those commands and streams.
A kata-shim
instance will both forward signals and stdin streams to the container process on the guest and pass the container stdout and stderr streams back up the stack to the CRI shim or Docker via the container process reaper. Kata-runtime
creates a kata-shim
daemon for each container and for each OCI command received to run within an already running container(example, docker exec
).
Guest image(Root filesystem image)
当运行docker run -ti ubuntu date
命令时:
- hypervisor将使用guest kernel启动mini-OS
systemd
运行在mini-OS上下文中,将在相同的环境下启动kata-agent
- 代理将创建一个新的受限的上下文来运行指定的命令(在此实例为date)
- 代理将在这个新的上下文中执行命令,并首先将根文件系统设置为预期的Ubuntu根文件系统
Agent
Kata-agent
是一个进程,运行在客户操作系统中用于管理容器以及容器内运行的进程。kata-agent
执行单元是由一系列命名空间(NS, UTS, IPC, PID)定义的沙箱。kata-runtime
能够在每个虚拟机中运行多个容器。在使用docker的情形中,kata-runtime
在每个pod中创造单一的容器。kata-agent
通过gRPC和其他的kata组件进行通信,其在同样的gRPC URL上运行一个yamux server. kata-agent
基于libcontainer
来管理容器的生命周期,这样kata-agent
就能够重用runc
的大部分代码。
Runtime
kata-runtime
是一个OCI兼容的容器运行时并负责处理所有OCI运行时规范定义的指令,启动kata-shim
实例。运行时的行为由configuration.toml
文件控制,该文件默认安装在/usr/share/defaults/kata-containers
环境下面。下面将介绍kata-runtime
如何处理一些最重要的OCI指令。
create
当处理OCIcreate
命令时,kata-runtime
要经过下列步骤:
-
创建网络命名空间用于启动虚拟机和shim进程;
-
执行pre-start hooks,其中一个负责在主机命名空间和新创建的网络命名空间之间建立
veth pair
; -
扫描新创建的网络命名空间,创建一个
MACVTAP
设备用于连接veth接口和虚拟机内的tap
接口;(引入MACVTAP
设备的目标是:简化虚拟机环境中的交换网络,代替传统的Linux Tap设备加Bridge设备的组合,更多相关信息可以参考:https://opengers.github.io/openstack/openstack-base-virtual-network-devices-tuntap-veth/ ,https://cloud.tencent.com/developer/article/1472358 ,下图引自参考链接) -
在创建的网络命名空间启动VM,并在VM中添加
tap
接口; -
等待VM就绪;
-
启动
kata-proxy
,kata-proxy
连接到创建好的虚拟机,该进程负责代理与虚拟机的所有通信流量。对于每个VM都对应着一个代理; -
通过代理与
kata-agent
通信在VM内部配置沙箱; -
与
kata-agent
通信创建容器,依赖向kata-runtime
提供的OCI配置文件config.json
,将基于libcontainer
在虚拟机内部启动容器主进程; -
启动
kata-shim
,它将连接到kata-proxy
提供的gRPC server上,kata-shim
用于获取容器进程在终止之前的输出(ReadStdout, ReadStderr
),以及VM内的容器进程终止时的返回码(WaitProcess
)。注意,kata-shim
是在网络命名空间内启动的,以允许上层确定已创建哪个网络命名空间并通过检查kata-shim进程来确定,这确保kata-shim
进程随着容器进程的终止而被杀死。
start
kata-runtime
将经过下列步骤:
- 首先通过
kata-proxy
与kata-agent
交互来启动VM内的容器工作进程。例如在容器内部执行的命令是top
,kata-shim
的ReadStdOut()
将会开始返回top
命令的文本输出,waitProcess()
只要top
进程还在运行就会一直阻塞; - 调用
post-start
hooks,通常并没有进行什么操作;
exec
OCI exec
命令允许在已经运行的容器中运行其他的命令,在kata containers中将执行下列步骤:
kata-runtime
通过代理向kata-agent
发起请求来在VM中正在运行的容器中启动一个新的进程;- 一个新的
kata-shim
进程被创建,并共享容器主进程对应的kata-shim
进程所在的网络和PID名称空间,这个新的kata-shim
被用于新的exec进程。(这个进程运行在VM内,与容器进程共享uts, pid, mnt, ipc命名空间)
kill
当发送OCI kill命令时,容器运行时将发送一个UNIX信号给容器进程来终止容器进程,例如SIGKILL
或者SIGTERM
。在传统的容器中,这意味着终止一个容器的运行;在kata containers中,这意味着终止容器以及与之关联的VM。其经过下列步骤:
- 通过代理向
kata-agent
发送请求来杀死容器进程; - 等待
kata-shim
进程退出; - 如果
kata-shim
进程超时仍然没有退出,强行杀死容器进程。通过向VM内的容器进程发送SIGKILL
信号; - 通过代理与
kata-agent
通信从VM移除容器配置; - 通过代理与
kata-agent
通信从VM移除沙箱配置; - 停止VM;
- 移除网络命名空间中所有的网络配置并删除网络命名空间;
- 执行post-stop hooks;