原文:https://learn.openshift.com/subsystems/container-internals-lab-2-0-part-4
一、容器引擎&Linux内核
如果在搜索引擎上搜索docker架构,会出现大量描述错误的架构设计或者只讲对了一部分。
为什么人们总是理解错误,有两个主要原因:
首先,大多数架构图都将docker守护进程画为容器主机上的蓝色框,显示容器运行在docker守护进程之上,那是错误的,容器不在docker上运行,docker引擎只是通用容器引擎的一个例子,人们将命令传达给docker引擎,docker引擎在将其传给Linux内核-事实上容器是由Linux内核创建、运行的。即使画对了容器引擎和内核的架构关系,他们也从未显示和容器引擎并排运行的容器。
其次,当设计图显示容器是Linux进程时,它们从不并排显示容器引擎。这导致人们从不把这两件事放在一起考虑,因此用户会对一部分内容感到困惑:
做一个简单的实验,使用top命令运行三个容器
docker run -td registry.access.redhat.com/ubi7/ubi top docker run -td registry.access.redhat.com/ubi7/ubi top podman run -td registry.access.redhat.com/ubi7/ubi top
现在来检查容器主机的进程表:
ps -efZ | grep -v grep | grep " top"
ps -efZ # 查看主体(进程)的安全上下文
grep -v grep # 排除掉带grep命令的进程
grep " top " # 查看带有top的进程
现在在每个容器中都使用了 top 命令,两个使用docker启动的,一个使用podman启动的。通过ps 命令我们发现他们就是常规的进程,因为容器化进程只是花哨的Linux进程,和普通进程之间存在额外的隔离。docker 守护进程和容器化进程是并排运行的,就像下图这样:
在内核中,没有单一的数据结构表示容器是什么。目前Linux社区的思想是提供许多从试验性的到非常成熟的技术,使用户能够以创造性的方式来将其混合在一起。这正是容器引擎所做的(docker,podman,CRI-O等),它利用内核去创建我们称之为容器等东西。容器的概念是人们创造出来的,而不是内核。这是Linux常见的模式:区分底层(kernel)技术和上层(userspace)技术,这允许内核开发者专注于新增技术,而用户可以尝试使用这些技术去更好的工作。
Linux内核只有一个主要的数据结构来追踪进程--进程id表。ps命令转储此数据结构的内容。但是这不是容器的全部定义:容器引擎追踪使用了哪些内核隔离技术,甚至是挂载了哪些数据卷,这些可以看作是容器的元数据。目前为止,我们应该明白容器化进程就是常规的Linux进程,使用namespace、selinux和cgroups内核技术将其隔离。有时将其描述为虚拟化的沙盒、隔离或者幻觉。
最后,容器化进程只是常规的Linux进程。所有的进程并排运行,无论是常规的Linux进程、长期运行的守护进程、批处理进程、你手动运行的交互式命令或者是虚拟化进程。所有这些进程都会向Linux内核请求受保护的资源像存储,RAM,TCP套接字等。
二、一步一步来创建一个容器
这一节看一下容器等基本构造,几乎所有符合OCI的容器引擎的容器基本构造都相同。
- 拉取/扩展/挂载镜像
- 创建符合OCI规范的文件
- 使用spec文件调用runc
1. 拉取/扩展/挂载镜像
Podman可让您轻松分解容器构造的每个步骤以进行学习。首先拉取镜像,拓展它,给容器创建一个新的覆盖文件系统作为容器的读/写根文件系统。为了做到这一点,使用一个特殊构造的容器镜像,来分解步骤,而不是一次全都开始。
podman create -dt --name on-off-container -v /mnt:/mnt:Z quay.io/fatherlinux/on-off-container
使用上面命令使用本地镜像创建一个容器,使用 podman ps -a 命令来查看状态,容器是created状态,而不是running。
使用mount命令来查看存储:mount | grep -v docker | grep merged 。发现没有任何结果,这是因为它被挂载在所谓的mount namespace中,你只能在容器内看到挂载。podman提供了叫做podman-mount的功能,你可以用这个功能在容器外查看挂载的路径:
podman mount on-off-container
返回的目录是容器使用的覆盖文件系统中的系统级挂载点。 您现在可以立即更改容器文件系统中的任何内容。 可以通过下面命令创建/test文件夹:
touch $(podman mount on-off-container)/test ls $(podman mount on-off-container)
现在可以看见test文件,下面在容器里运行shell的时候你同样可以看到这个test文件。
2. 创建spec文件
容器已经存在本地并挂载了数据卷,但是目前还没有runc的spec文件。手动创建spec文件非常繁琐,因为他们是由许多不同选项的复杂JSON组成的(由OCI 运行时规范控制),幸运的是,容器引擎会帮我们创建这个spec文件,任何OCI兼容的运行时都可以使用此完全相同的规范文件(runc,crun,katacontainer,gvisor等)。首先来检查一下它在哪里:
cat /var/lib/containers/storage/overlay-containers/$(podman ps -l -q --no-trunc)/userdata/config.json|jq .
#jq . 规范json文件
这命令现在会报错因为容器引擎还没有创建它,现在使用podman和一个特点的容器镜像来创建这个文件:
podman start on-off-container
现在config.json文件已经被创建,继续使用上面的cat 命令检查这个文件,其中有一些选项与podman选项十分相似,这spec文件真正突出了API。
podman此时并未启动容器,使用podman ps -a查看 发现STATUS是Exited。使用inspect命令检查细节,发现:
# -f filename 如果 filename为常规文件,则为真 。shell 命令
即如果/mnt下面由on文件,容器才会运行top命令。
3. 调用运行时
现在我们有了存储和一个config.json,通过config.json创建一个虚拟化进程。我们已经构建了一个容器镜像,它只有在/mnt/on文件存在的情况下才会创建一个进程,现在来创建这个on文件:touch /mnt/on,并启动容器podman start on-off-container,继续使用podman ps -a命令查看容器状态,发现此时的状态是up。使用exec命令在容器中创建交互式终端:
podman exec -it on-off-container bash
使用ls -alh命令查看,发现结果和预期的一样,test文件就存在那。我们通过三个基本步骤才真正创建了一个容器
三、ELinux和sVirt:动态生成上下文来保护您的容器
运行下面命令,得到如下结果:
$ podman run -dt registry.access.redhat.com/ubi7/ubi sleep 10 244b6bda438e899db9d055335dbd015b93a50e5de7277f8ac0919c65f9b32470 $ podman run -dt registry.access.redhat.com/ubi7/ubi sleep 10 a2652ee6f9e4b4b849fedcbad4872c36e1cef398fa6b00e90988811237bfbbf8 $ sleep 3 $ ps -efZ | grep container_t | grep sleep system_u:system_r:container_t:s0:c666,c809 root 25067 25023 7 14:03 pts/0 00:00:00 sleep 10 system_u:system_r:container_t:s0:c471,c835 root 25189 25174 7 14:03 pts/0 00:00:00 sleep 10 $
请注意,每个容器都标有动态生成的多级安全性(Multi Level Security (MLS))标签。例如上面结果第一个MLS标签是c66,c809,第二个MLS标签是c471,c835。因此每个容器启动都会生成一个不同的MLS标签,它们被阻止访问彼此的存储和文件等。
SELinux不只是标记进程,它还必须标记进程访问的文件,下面为数据创建一个/tmp/selinux-test目录,并检查目录上的SELinux标签。注意,类型设置为“user_tmp_t”,但没有设置MLS标签:
$ ls -alhZ /tmp/selinux-test/ drwxr-xr-x. root root unconfined_u:object_r:user_tmp_t:s0 . drwxrwxrwt. root root system_u:object_r:tmp_t:s0 .. $
多启动几次同一个镜像,发现每次多MLS都不一样,这是sVirt在工作:
$ podman run -t -v /tmp/selinux-test:/tmp/selinux-test:Z registry.access.redhat.com/ubi7/ubi ls -alhZ /tmp/selinux-test drwxr-xr-x. root root system_u:object_r:container_file_t:s0:c729,c736 . drwxrwxrwt. root root system_u:object_r:container_file_t:s0:c729,c736 .. $ podman run -t -v /tmp/selinux-test:/tmp/selinux-test:Z registry.access.redhat.com/ubi7/ubi ls -alhZ /tmp/selinux-test drwxr-xr-x. root root system_u:object_r:container_file_t:s0:c363,c698 . drwxrwxrwt. root root system_u:object_r:container_file_t:s0:c363,c698 .. $ podman run -t -v /tmp/selinux-test:/tmp/selinux-test:Z registry.access.redhat.com/ubi7/ubi ls -alhZ /tmp/selinux-test drwxr-xr-x. root root system_u:object_r:container_file_t:s0:c479,c515 . drwxrwxrwt. root root system_u:object_r:container_file_t:s0:c479,c515 .. $
最终,查看/tmp/selinux-test目录多MLS,发现始终和最后一次运行的容器的MLS相同。:Z选项会自动标记和绑定挂载,以便容器可以访问和更改挂载点上的文件。 这样可以防止任何其他进程访问此数据,并且对最终用户是透明的。
note:MLS变更后,之前启动的容器便无法访问挂载的容器数据卷。只有MLS相同才可以。
四、Cgroups:使用容器实例动态创建
本节的目的是为了了解容器如何防止使用彼此的保留资源.Linux内核有一个称为cgroups (control groups的缩写)的特性,它限制、说明和隔离进程的资源使用(CPU、内存、磁盘I/O、网络等)。通常,这些控制组是由系统管理员(使用cgexec)设置的,或者使用systemd (system -run--slice)配置的,但是使用容器引擎时,会自动处理这种配置。
podman run -dt registry.access.redhat.com/ubi7/ubi sleep 10 podman run -dt registry.access.redhat.com/ubi7/ubi sleep 10 sleep 3 for i in $(podman ps | grep sleep | awk '{print $1}' | grep [0-9]); do find /sys/fs/cgroup/ | grep $i; done
容器引擎自动将容器化进程放进自己的cgroup中,这是十分方便的,类似sVirt。
五、SECCOMP:限制容器化进程与内核交互的方式
可以将SECCOMP看过是防火墙,它可以阻止某些系统调用。虽然这是可选的,并且默认情况下是关闭的。但它是一个可以用来阻挡不正常容器的十分强大的工具。
$ cat ~/labs/lab3-step5/chmod.json { "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [ { "name": "fchmodat", "action": "SCMP_ACT_ERRNO" } ] } $ podman run -it --security-opt seccomp=./labs/lab3-step5/chmod.json registry.access.redhat.com/ubi7/ubi chmod 777 /etc/hosts chmod: changing permissions of '/etc/hosts': Operation not permitted $
# fchmodat - change permissions of a file relative to a directory file descriptor
更改/etc/hosts权限的行为被阻止