cntr
不要在容器中使用 $ apt install vim
!
cntr
是 docker exec
的替代品,可以让你带上所有开发工具。
它通过使用 FUSE 文件系统创建嵌套容器,将一个容器或主机的文件系统挂载到目标容器中。
这样可以在生产环境中使用最小化的运行时镜像,并限制漏洞利用的表面。
Cntr 还发表在 Usenix ATC 2018 上。 引用请参见 bibtex。
演示
在这个两分钟的录像中,你可以学习 cntr 的所有基础知识:
特性
- 为了方便,cntr 原生支持以下容器引擎的容器名称/标识符:
- docker
- podman
- LXC
- LXD
- rkt
- systemd-nspawn
- containerd
- 对于其他容器引擎,cntr 也可以使用进程 ID (PID) 而不是容器名称。
安装
Cntr 只支持 Linux。
预编译的静态链接二进制文件
对于 linux x86_64,我们为每个发布版本构建静态二进制文件。可以根据需求添加更多平台。 预编译的压缩包可以在发布页面找到。 运行时只需要相关容器引擎的命令行工具。
从源码构建
编译只需要 rust + cargo。 查看 rustup.rs 了解如何获取可用的 rust 工具链。 然后运行:
方法一:
$ cargo install cntr
或者安装最新的主分支:
$ cargo install --git https://github.com/Mic92/cntr
对于离线构建,我们还提供了一个包含所有依赖的压缩包,可以在这里找到,用于使用 cargo-vendor 进行编译。
使用方法
Cntr 主要提供两个子命令:attach
和 exec
:
attach
:允许你使用自己的本地 shell/命令附加到容器。 Cntr 会将容器挂载到/var/lib/cntr
。 容器本身将不受影响地运行,因为挂载更改对容器进程不可见。- 示例:
cntr attach <container_id>
,其中container_id
可以是容器标识符或进程 ID(参见下面的示例)。
- 示例:
exec
:一旦进入容器,你还可以运行容器文件系统中的命令。由于这些命令可能需要它们的原生挂载布局在/
而不是/var/lib/cntr
,cntr 提供exec
子命令再次 chroot 到容器,并重置可能被 shell 更改的环境变量。- 示例:
cntr exec <command>
,其中command
是容器中的可执行文件
- 示例:
注意:Cntr 需要在与容器相同的主机上运行。如果容器在虚拟机中运行而 cntr 在虚拟机管理程序上运行,则无法工作。
$ cntr --help
Cntr 1.5.1
Jörg Thalheim <joerg@thalheim.io>
进入或在容器中执行命令
用法:
cntr <子命令>
选项:
-h, --help 打印帮助信息
-V, --version 打印版本信息
子命令:
attach 进入容器
exec 在容器文件系统中执行命令
help 打印此消息或给定子命令的帮助
$ cntr attach --help
cntr-attach 1.5.1
Jörg Thalheim <joerg@thalheim.io>
进入容器
用法:
cntr attach [选项] <id> [命令]...
选项:
-h, --help 打印帮助信息
--effective-user <EFFECTIVE_USER> 应该成为主机上新创建文件所有者的有效用户名
-t, --type <TYPE> 要尝试的容器类型(用','分隔)。[默认:除 command 外的所有类型]
[可能的值:process_id, rkt, podman, docker, nspawn, lxc, lxd,
containerd, command]
参数:
<id> 容器 ID、容器名称或进程 ID
<命令>... 附加后要执行的命令及其参数。考虑在前面加上 '-- ' 以防止解析 '-x' 类的标志。[默认:$SHELL]
$ cntr exec --help
cntr-exec 1.5.1
Jörg Thalheim <joerg@thalheim.io>
在容器文件系统中执行命令
用法:
cntr exec [命令]...
选项:
-h, --help 打印帮助信息
-V, --version 打印版本信息
参数:
<命令>... 附加后要执行的命令及其参数。考虑在前面加上 '-- ' 以防止解析 '-x' 类的标志。[默认:$SHELL]
Docker
1: 找出容器名称/容器 ID:
$ docker run --name boxbusy -ti busybox
$ docker ps
容器 ID 镜像 命令 创建时间 状态 端口 名称
55a93d71b53b busybox "sh" 22 秒前 运行中 boxbusy
可以提供容器 ID...
$ cntr attach 55a93d71b53b
[root@55a93d71b53b:/var/lib/cntr]# echo "我在一个容器里!"
[root@55a93d71b53b:/var/lib/cntr]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
40: eth0@if41: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
[root@55a93d71b53b:/var/lib/cntr]# vim etc/resolv.conf
...或容器名称。
使用cntr exec
来执行容器原生命令(在cntr shell中运行时)。
$ cntr attach boxbusy
[root@55a93d71b53b:/var/lib/cntr]# cntr exec -- sh -c 'busybox | head -1'
你也可以使用此仓库中的Dockerfile来构建一个带有cntr的docker容器:
$ docker build -f Dockerfile . -t cntr
# boxbusy是要附加到的目标容器的名称
$ docker run --pid=host --privileged=true -v /var/run/docker.sock:/var/run/docker.sock -ti --rm cntr attach boxbusy /bin/sh
Podman
参见docker用法,只需将docker
替换为podman
命令。
LXD
1: 创建一个容器并启动它
$ lxc image import images:/alpine/edge
$ lxc launch images:alpine/edge
$ lxc list
+-----------------+---------+------+------+------------+-----------+
| 名称 | 状态 | IPV4 | IPV6 | 类型 | 快照数量 |
+-----------------+---------+------+------+------------+-----------+
| amazed-sailfish | RUNNING | | | PERSISTENT | 0 |
+-----------------+---------+------+------+------------+-----------+
2: 使用cntr附加到容器
$ cntr attach amazed-sailfish
$ cat etc/hostname
amazed-sailfish
LXC
1: 创建一个容器并启动它
$ lxc-create --name ubuntu -t download -- -d ubuntu -r xenial -a amd64
$ lxc-start --name ubuntu -F
...
Ubuntu 16.04.4 LTS ubuntu console
ubuntu login:
$ lxc-ls
ubuntu
2: 使用cntr附加到容器:
$ cntr attach ubuntu
[root@ubuntu2:/var/lib/cntr]# cat etc/os-release
NAME="Ubuntu"
VERSION="16.04.4 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.4 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial
rkt
1: 查找容器uuid:
$ rkt run --interactive=true docker://busybox
$ rkt list
UUID APP 镜像名称 状态 创建时间 启动时间 网络
c2d2e87e busybox registry-1.docker.io/library/busybox:latest 运行中 6分钟前 6分钟前 default:ip4=172.16.28.3
2: 使用cntr附加
# 确保你的容器仍在运行!
$ cntr attach c2d2e87e
# 终于不是旧的丑陋的top了!
[gen0@rkt-c2d2e87e-e798-4341-ae93-26f6cbb7c017:/var/lib/cntr]# htop
...
使用cntr你还可以调试rkt的stage1 - 即使rkt本身不支持。
$ ps aux | grep stage1
joerg 13546 0.0 0.0 120808 1608 pts/12 S+ 11:10 0:00 grep --binary-files=without-match --directories=skip --color=auto stage1
root 22232 0.0 0.0 54208 2656 pts/7 S+ 10:54 0:00 stage1/rootfs/usr/lib/ld-linux-x86-64.so.2 stage1/rootfs/usr/bin/systemd-nspawn --boot --notify-ready=yes --register=true --link-journal=try-guest --quiet --uuid=c2d2e87e-e798-4341-ae93-26f6cbb7c017 --machine=rkt-c2d2e87e-e798-4341-ae93-26f6cbb7c017 --directory=stage1/rootfs --capability=CAP_AUDIT_WRITE,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FSETID,CAP_FOWNER,CAP_KILL,CAP_MKNOD,CAP_NET_RAW,CAP_NET_BIND_SERVICE,CAP_SETUID,CAP_SETGID,CAP_SETPCAP,CAP_SETFCAP,CAP_SYS_CHROOT -- --default-standard-output=tty --log-target=null --show-status=0
因此我们使用进程ID而不是容器uuid:
$ cntr attach 22232
# 新的令人兴奋的领域!
[root@turingmachine:/var/lib/cntr]# mount | grep pods
sysfs on /var/lib/cntr/var/lib/rkt/pods/run/c2d2e87e-e798-4341-ae93-26f6cbb7c017/stage1/rootfs/sys type sysfs (ro,nosuid,nodev,noexec,relatime)
tmpfs on /var/lib/cntr/var/lib/rkt/pods/run/c2d2e87e-e798-4341-ae93-26f6cbb7c017/stage1/rootfs/sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /var/lib/cntr/var/lib/rkt/pods/run/c2d2e87e-e798-4341-ae93-26f6cbb7c017/stage1/rootfs/sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
systemd-nspawn
1: 启动容器
$ wget https://cloud-images.ubuntu.com/releases/16.04/release/ubuntu-16.04-server-cloudimg-amd64-root.tar.xz
$ mkdir /var/lib/machines/ubuntu
$ tar -xf ubuntu-16.04-server-cloudimg-amd64-root.tar.xz -C /var/lib/machines/ubuntu
$ systemd-nspawn -b -M ubuntu
$ machinectl list
MACHINE CLASS SERVICE OS VERSION ADDRESSES
ubuntu container systemd-nspawn ubuntu 16.04 -
2: 附加
$ cntr attach ubuntu
通用进程ID
cntr需要的最少信息是你想要附加的容器进程的进程ID。
# 你知道chromium也使用命名空间吗?
$ ps aux | grep 'chromium --type=renderer'
joerg 17498 11.7 1.0 1394504 174256 ? Sl 15:16 0:08 /usr/bin/chromium
在这种情况下,17498就是我们要找的pid。
$ cntr attach 17498
# 看起来和我们的系统很相似,但用户更少
[joerg@turingmachine cntr]$ ls -la /
total 240
drwxr-xr-x 23 nobody nogroup 23 Mar 13 15:05 .
drwxr-xr-x 23 nobody nogroup 23 Mar 13 15:05 ..
drwxr-xr-x 2 nobody nogroup 3 Mar 13 15:14 bin
drwxr-xr-x 4 nobody nogroup 16384 Jan 1 1970 boot
drwxr-xr-x 24 nobody nogroup 4120 Mar 13 14:56 dev
drwxr-xr-x 52 nobody nogroup 125 Mar 13 15:14 etc
drwxr-xr-x 3 nobody nogroup 3 Jan 8 16:17 home
drwxr-xr-x 8 nobody nogroup 8 Feb 9 22:10 mnt
dr-xr-xr-x 306 nobody nogroup 0 Mar 13 09:38 proc
drwx------ 22 nobody nogroup 43 Mar 13 15:09 root
...
Containerd
对于containerd集成,需要ctr
二进制文件。你可以通过运行以下命令获取二进制文件:
$ GOPATH=$(mktemp -d)
$ go get github.com/containerd/containerd/cmd/ctr
$ $GOPATH/bin/ctr --help
将生成的ctr
二进制文件放入你的$PATH
中
1: 启动容器
$ ctr images pull docker.io/library/busybox:latest
$ ctr run docker.io/library/busybox:latest boxbusy
$ ctr tasks lists
TASK PID STATUS
boxbusy 24310 RUNNING
2: 附加
$ cntr attach boxbusy
也可以从容器本身运行cntr。 这个仓库包含了一个示例Dockerfile:
$ docker build -f Dockerfile.example . -t cntr
$ docker save cntr > cntr.tar
$ ctr images import --base-name cntr ./cntr.tar
在这个例子中,我们通过进程ID附加到containerd。任务的进程ID在ctr tasks list
中给出。
$ ctr run --privileged --with-ns pid:/proc/1/ns/pid --tty docker.io/library/cntr:latest cntr /usr/bin/cntr attach 31523 /bin/sh
要解析containerd名称,还需要将ctr
二进制文件(~12MB)添加到Dockerfile中。
附加配置
ZFS
cntr
需要在ZFS下启用POSIX ACL。默认情况下,Linux ZFS没有启用POSIX ACL。这会导致在尝试attach
时出现以下错误:
unable to move container mounts to new mountpoint: EOPNOTSUPP: Operation not supported on transport endpoint
要在ZFS数据集上启用POSIX ACL:
$ zfs set acltype=posixacl zpool/media
$ zfs set xattr=sa zpool/media # 可选,但建议使用以获得最佳性能
工作原理
Cntr与容器无关:它不与容器引擎交互,而是实现底层操作系统API。它将每个容器视为一组可以继承属性的进程。
Cntr继承以下容器属性:
- 命名空间(mount、uts、pid、net、cgroup、ipc)
- Cgroups
- Apparamor/selinux
- 能力
- 用户/组ID
- 环境变量
- 以下文件:/etc/passwd、/etc/hostname、/etc/hosts、/etc/resolv.conf
在底层,它会生成一个继承容器完整上下文的shell或用户定义的程序,并将自身作为fuse文件系统挂载。
我们使用xfstests和广泛的文件系统性能基准测试(iozone、pgbench、dbench、fio、fs-mark、postmark等)对cntr文件系统的正确性和性能进行了广泛的评估。
相关项目
Bibtex
我们在Usenix ATC 2018上发表了一篇包含Cntr所有技术细节的论文。
@inproceedings{cntr-atc18,
author = {J{\"o}rg Thalheim and Pramod Bhatotia and Pedro Fonseca and Baris Kasikci},
title = {Cntr: 轻量级操作系统容器},
booktitle = {2018 {USENIX} 年度技术会议 ({USENIX} {ATC} 18)},
year = {2018},
}