FreeBSD Thin(薄) Jail

FreeBSD Thin Jail是基于 ZFS (OpenZFS) 快照 或模板 和 NullFS 来创建的 Jail

设置环境变量

为方便调整,我设置了环境变量来方便后续操作

设置 jail目录和release版本环境变量
export jail_dir="zdata/jails"
export bsd_ver="14.3"
# 在FreeBSD中root用户的shell默认是sh,所以调整 ~/.shrc
echo 'jail_dir="zdata/jails"' >> ~/.shrc
echo 'bsd_ver="14.3"' >> ~/.shrc

OpenZFS快照Thin Jail

  • 为模板创建发行版,这个是只读的:

在ZFS中创建模板数据集
zfs create -p zroot/jails/templates/14.2-RELEASE
下载用户空间
fetch https://download.freebsd.org/ftp/releases/amd64/amd64/$bsd_ver-RELEASE/base.txz -o /$jail_dir/media/$bsd_ver-RELEASE-base.txz
  • 将下载内容解压缩到模板目录:

内容解压缩到模板目录
tar -xf /usr/local/jails/media/14.2-RELEASE-base.txz -C /usr/local/jails/templates/14.2-RELEASE --unlink
  • 将时区和DNS配置复制到模板目录:

时区和DNS配置复制到模板目录
cp /etc/resolv.conf /usr/local/jails/templates/14.2-RELEASE/etc/resolv.conf
cp /etc/localtime /usr/local/jails/templates/14.2-RELEASE/etc/localtime
  • 更新模板补丁:

更新补丁
freebsd-update -b /usr/local/jails/templates/14.2-RELEASE/ fetch install
  • 从模板创建 ZFS 快照:

模板创建 ZFS 快照
zfs snapshot zroot/jails/templates/14.2-RELEASE@base

一旦创建了 OpenZFS 快照,就可以使用 OpenZFS 克隆功能创建无限个 jail

从快照中clone出2个Thin Jail
zfs clone zroot/jails/templates/14.2-RELEASE@base zroot/jails/containers/dev-1
zfs clone zroot/jails/templates/14.2-RELEASE@base zroot/jails/containers/dev-2

# 再增加一个dev
zfs clone zroot/jails/templates/14.2-RELEASE@base zroot/jails/containers/dev
  • 准备 dev-1 Thin Jail配置 /etc/jail.conf.d/dev-1.conf ( dev-2.conf 类似,只有主机名和IP不同):

dev-1.conf
dev-1 {
  # STARTUP/LOGGING
  exec.start = "/bin/sh /etc/rc";
  exec.stop = "/bin/sh /etc/rc.shutdown";
  exec.consolelog = "/var/log/jail_console_${name}.log";

  # PERMISSIONS
  allow.raw_sockets;
  exec.clean;
  mount.devfs;

  # HOSTNAME/PATH
  host.hostname = "${name}";
  path = "/usr/local/jails/containers/${name}";

  # NETWORK
  # ip4 = inherit;
  ip4.addr = 10.0.0.10/24;
  interface = wifibox0;
}
  • 启动Thin Jail容器:

启动2个Thin Jail
service jail start dev-1
service jail start dev-2

# 再启动一个dev
service jail start dev

警告

OpenZFS快照Thin Jail实际上有一个 致命限制 : ZFS snapshot是不可更改的

虽然 snapshot => clone 极大地节约了磁盘空间,但是如果需要更新OpenZFS snapshot Thin Jails,需要:

  • destroy 所有的 clone,也就是销毁Thin Jails

  • 更新ZFS卷,也就是上文案例中的 zroot/jails/templates/14.3-RELEASE ,当然可以是再建立一个卷,例如 zroot/jails/templates/14.3-RELEASE

  • 重新走一遍 ZFS snapshot => ZFS clone ,来构建Thin Jails

也就是说,这是一个重建的过程,和 Docker 的镜像发布非常相似: 不存在更新snapshot从而达到所有clone自动更新的效果

好处是和 Docker 容器镜像发布是一个原理,如果你熟悉容器管理,其实就是一样的技术。在大规模部署时,假设类似 Kubernetes ,可以采用滚动销毁旧Jail,然后从新的snapshot上clone出新的Jail来实现升级。

相关讨论可以参考 Upgrade thin jail with ZFS snapshot

NullFS thin Jail

备注

目前我主要使用 NullFS thin Jail ,在 VNET + Thin Jail 中就是采用这种方式。此外,我在 FreeBSD桌面 环境也采用 NullFS thin Jail ,但没有结合 FreeBSD VNET Jail (因为桌面没有多个IP地址可以分配),具体案例见 桌面环境 Thin Jail

通过结合 Thin Jail 和 NullFS 技术也可以创建节约系统文件存储开销(类似于 ZFS snapshot clone出来的卷完全不消耗空间),并且能够将Host主机的我呢见目录共享给 多个 Jail。

备注

实际上直接使用 ZFS snapshot 创建 thin jail 和 使用 NullFS 创建 thin jail 的区别在于:

  • ZFS snapshot 是只读的,不可更改的 ,这意味着 没有办法简单通过更新共享的ZFS snapshot来实现Thin Jail操作系统

  • NullFS直接使用ZFS dataset是可读写的 ,也就是 直接更新NullFS底座共享的ZFS dataset可以瞬间更新所有Thin Jail

所以,两者区别仅在于创建Thin Jail的开始步骤:

  • 将FreeBSD Release base存放在 只读14.3-RELEASE@base 快照 - OpenZFS snapshot Thin Jail

  • 将FreeBSD Relaase base存放在 读写14.3-RELEASE-base 数据集 - NullFS Thin Jail

备注

这里创建的 Thin Jail Using NullFS 是基于 ZFS 完成的实践。实际上,基于NullFS的Thin Jail也可以使用传统的UFS构成。由于我在阿里云租用的VM默认文件系统是UFS,所以我独立再记录到 在UFS文件系统上构建Thin Jail 中。

  • 创建 读写模式14.3-RELEASE-base (注意,大家约定俗成 @base 表示只读快照, -base 表示可读写数据集)

创建 读写模式14.3-RELEASE-base 数据集
zfs create -p $jail_dir/templates/$bsd_ver-RELEASE-base
下载用户空间
fetch https://download.freebsd.org/ftp/releases/amd64/amd64/$bsd_ver-RELEASE/base.txz -o /$jail_dir/media/$bsd_ver-RELEASE-base.txz
  • 将下载内容解压缩到模板目录:

内容解压缩到模板目录( 14.3-RELEASE-base 后续不需要创建快照,直接使用)
tar -xf /$jail_dir/media/$bsd_ver-RELEASE-base.txz -C /$jail_dir/templates/$bsd_ver-RELEASE-base --unlink
  • 将时区和DNS配置复制到模板目录:

时区和DNS配置复制到模板目录
cp /etc/resolv.conf /$jail_dir/templates/$bsd_ver-RELEASE-base/etc/resolv.conf
cp /etc/localtime   /$jail_dir/templates/$bsd_ver-RELEASE-base/etc/localtime
  • 更新模板补丁:

更新补丁
freebsd-update -b /$jail_dir/templates/$bsd_ver-RELEASE-base/ fetch install

关键部分来了,以下是NullFS特别部分

  • 创建一个特定数据集 skeleton (骨骼) ,这个 "骨骼" skeleton 命名非常形象,用意就是构建特殊的支持大量thin jial的框架底座

创建特定数据集 skeleton (骨骼)
zfs create -p $jail_dir/templates/$bsd_ver-RELEASE-skeleton
  • 执行以下命令,将特定目录移入 skeleton 数据集,并构建 baseskeleton 必要目录的软连接关系(注意:刚开始步骤有报错,我修改了方法)

创建目录(以前的方法,实际有报错,修正见后文)
mkdir -p /usr/local/jails/templates/14.2-RELEASE-skeleton/home
mkdir -p /usr/local/jails/templates/14.2-RELEASE-skeleton/usr
# etc目录包含发行版提供的配置文件
mv /usr/local/jails/templates/14.2-RELEASE-base/etc /usr/local/jails/templates/14.2-RELEASE-skeleton/etc
# local目录是空的
mv /usr/local/jails/templates/14.2-RELEASE-base/usr/local /usr/local/jails/templates/14.2-RELEASE-skeleton/usr/local
# tmp 目录是空的
mv /usr/local/jails/templates/14.2-RELEASE-base/tmp /usr/local/jails/templates/14.2-RELEASE-skeleton/tmp
# var 目录有很多预存目录,其中移动时有报错显示 var/empty 目录没有权限
mv /usr/local/jails/templates/14.2-RELEASE-base/var /usr/local/jails/templates/14.2-RELEASE-skeleton/var
# root 目录是管理员目录,有基本profile文件
mv /usr/local/jails/templates/14.2-RELEASE-base/root /usr/local/jails/templates/14.2-RELEASE-skeleton/root

这里有一个报错,在 mv varskeleton ZFS数据集下时会报错,显示其中 var/empty 目录没有权限删除: mv var/empty: Operation not permitted ,我后来是通过将上一级目录重命名来解决的 mv var var.bak ,具体方法按照 VNET + Thin Jail :

特定目录移入 skeleton 数据集
mkdir -p /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/home
mkdir -p /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/usr
# 我单独创建一个docs目录用于后续将host主机的docs映射到jail中
mkdir -p /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/docs

# etc目录包含发行版提供的配置文件
mv /$jail_dir/templates/$bsd_ver-RELEASE-base/etc /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/etc
# local目录是空的
mv /$jail_dir/templates/$bsd_ver-RELEASE-base/usr/local /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/usr/local
# tmp 目录是空的
mv /$jail_dir/templates/$bsd_ver-RELEASE-base/tmp /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/tmp
# var 目录有很多预存目录,但是直接mv移动时有报错显示 var/empty 目录没有权限,这会导致目标目录破坏,所以改为rsync同步
rsync -avz /$jail_dir/templates/$bsd_ver-RELEASE-base/var /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/var
mv /$jail_dir/templates/$bsd_ver-RELEASE-base/var /$jail_dir/templates/$bsd_ver-RELEASE-base/var.bak
# root 目录是管理员目录,有基本profile文件
mv /$jail_dir/templates/$bsd_ver-RELEASE-base/root /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/root
  • 执行以下命令创建软连接

创建 skeleton 软连接
cd /$jail_dir/templates/$bsd_ver-RELEASE-base/
mkdir skeleton
ln -s skeleton/etc etc
ln -s skeleton/home home
ln -s skeleton/root root
ln -s ../skeleton/usr/local usr/local
ln -s skeleton/tmp tmp
ln -s skeleton/var var
# 这里增加一个docs目录用于存放数据(从Host主机映射到jail中)
ln -s skeleton/docs docs
  • 补充步骤 (实践发现需要修正certs软连接,见 VNET + Thin Jail ) 修复 /etc/ssl/certs 目录下证书文件软链接

修复软链接
#cd /$jail_dir/templates/$bsd_ver-RELEASE-base/etc/ssl/certs
cd /$jail_dir/templates/$bsd_ver-RELEASE-skeleton/etc/ssl/certs

# 先保存一份原始列表记录
ls -lh > /tmp/fix_link.txt

# 生成unlink命令
ls | sed "s@^@unlink @" > /tmp/unlink.sh

# 生成fix命令
# 这里不能使用绝对路径链接,否则会报错 link: ffdd40f9.0: Cross-device link
# ls -lh | awk '{print $NF, $(NF-2)}' | cut -c 9- | tail -n +2 | sed  "s@^@link @" > /tmp/fix_link.sh
ls -lh | awk '{print $NF, $(NF-2)}' | tail -n +2 | sed 's@^@ln -s ../@' > /tmp/fix_link.sh

# 检查一下 /tmp/fix_link.sh 是否满足要求
# 没有问题在执行以下2条命令

sh /tmp/unlink.sh
sh /tmp/fix_link.sh
  • skeleton 就绪之后,需要将数据复制到 jail 目录(如果是UFS文件系统),对于ZFS则非常方便使用快照:

将刚才建立的 skeleton 创建快照,也就是用于jail
zfs snapshot $jail_dir/templates/$bsd_ver-RELEASE-skeleton@base
# 假设这里创建名为dev的jail
jail_name=dev
zfs clone $jail_dir/templates/$bsd_ver-RELEASE-skeleton@base $jail_dir/containers/$jail_name

完成后可以看到相关的ZFS数据集如下:

完成后可以看到ZFS挂载(df)
Filesystem                                     Size    Used   Avail Capacity  Mounted on
zroot/ROOT/default                             192G    1.2G    191G     1%    /
...
zdata/jails                                    6.1T    112K    6.1T     0%    /zdata/jails
zdata/jails/media                              6.1T    197M    6.1T     0%    /zdata/jails/media
zdata/jails/templates                          6.1T    104K    6.1T     0%    /zdata/jails/templates
zdata/jails/containers                         6.1T     96K    6.1T     0%    /zdata/jails/containers
zdata/jails/templates/14.3-RELEASE-base        6.1T    446M    6.1T     0%    /zdata/jails/templates/14.3-RELEASE-base
zdata/jails/templates/14.3-RELEASE-skeleton    6.1T    4.4M    6.1T     0%    /zdata/jails/templates/14.3-RELEASE-skeleton
zdata/jails/containers/dev                     6.1T    4.4M    6.1T     0%    /zdata/jails/containers/dev
  • 创建一个 base template的目录,这个目录是 skeleton 挂载所使用的根目录:

创建一个jail所使用的模板目录
# 创建 dev 所使用的nullfs模板目录,这个目录就是jail的根PATH
# 和常规Jail仅命名 ${name} 不同,采用 ${name}-nullfs-base
mkdir -p /$jail_dir/$jail_name-nullfs-base
  • 之前构建 d2l ZFS snapshot Thin Jail时,共用 /etc/jail.conf :

共用的 /etc/jail.conf
# STARTUP/LOGGING
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.consolelog = "/var/log/jail_console_${name}.log";

# PERMISSIONS
allow.raw_sockets;
exec.clean;
mount.devfs;

# HOSTNAME/PATH
host.hostname = "${name}";

# NETWORK
#ip4 = inherit;
interface = wifibox0;

.include "/etc/jail.conf.d/*.conf";

以及 /etc/jail.conf.d/dev.conf (其中网络部分是 FreeBSD VNET Jail 配置,可忽略):

/etc/jail.conf.d/dev.conf 配置
dev {
  # 这里 devfs_ruleset 和Linux Jail的4不同
  devfs_ruleset=5;
  # 去除了 ip4.addr 配置

  # HOSTNAME/PATH
  path = "/usr/local/jails/${name}-nullfs-base";
 
  # VNET/VIMAGE
  vnet;
  vnet.interface = "${epair}b";

  # NETWORKS/INTERFACES
  $id = "10";
  $ip = "10.0.0.${id}/24";
  $gateway = "10.0.0.1";
  $bridge = "wifibox0"; 
  $epair = "epair${id}";

  # ADD TO bridge INTERFACE
  exec.prestart += "ifconfig ${epair} create up";
  exec.prestart += "ifconfig ${epair}a up descr jail:${name}";
  exec.prestart += "ifconfig ${bridge} addm ${epair}a up";
  exec.start    += "ifconfig ${epair}b ${ip} up";
  exec.start    += "route add default ${gateway}";
  exec.poststop = "ifconfig ${bridge} deletem ${epair}a";
  exec.poststop += "ifconfig ${epair}a destroy";
  
  # MOUNT
  mount.fstab = "/usr/local/jails/${name}-nullfs-base.fstab";
}
  • 注意,这里配置引用了一个针对nullfs的fstab配置,所以还需要创建一个 /usr/local/jails/dev-nullfs-base.fstab

/usr/local/jails/dev-nullfs-base.fstab 配置 dev Jail的nullfs挂载
/usr/local/jails/templates/14.2-RELEASE-base  /usr/local/jails/dev-nullfs-base/         nullfs  ro  0 0
/usr/local/jails/containers/dev               /usr/local/jails/dev-nullfs-base/skeleton nullfs  rw  0 0
  • 启动 dev :

启动 dev jail
service jail start dev

然后通过 rexec dev 进入jail进行观察,就会明白 NullFS 构建的 FreeBSD Thin(薄) Jail 巧妙之处:

进入 dev 检查
# df -h
Filesystem                                      Size    Used   Avail Capacity  Mounted on
/usr/local/jails/templates/14.2-RELEASE-base    893G    446M    893G     0%    /

# ls -lh /
total 159
-rw-r--r--   1 root wheel  1.0K Nov 29 17:58 .cshrc
-rw-r--r--   1 root wheel  495B Nov 29 17:58 .profile
-r--r--r--   1 root wheel  6.0K Nov 29 18:42 COPYRIGHT
drwxr-xr-x   2 root wheel   49B Nov 29 17:58 bin
drwxr-xr-x  14 root wheel   68B Nov 29 18:42 boot
dr-xr-xr-x  11 root wheel  512B Jan  9 15:02 dev
lrwxr-xr-x   1 root wheel   12B Jan  9 13:07 etc -> skeleton/etc
lrwxr-xr-x   1 root wheel   13B Jan  9 13:08 home -> skeleton/home
drwxr-xr-x   4 root wheel   78B Nov 29 18:20 lib
drwxr-xr-x   3 root wheel    5B Nov 29 17:57 libexec
drwxr-xr-x   2 root wheel    2B Nov 29 17:54 media
drwxr-xr-x   2 root wheel    2B Nov 29 17:54 mnt
drwxr-xr-x   2 root wheel    2B Nov 29 17:54 net
dr-xr-xr-x   2 root wheel    2B Nov 29 17:54 proc
drwxr-xr-x   2 root wheel  150B Nov 29 18:15 rescue
lrwxr-xr-x   1 root wheel   13B Jan  9 13:08 root -> skeleton/root
drwxr-xr-x   2 root wheel  150B Nov 29 18:23 sbin
drwxr-xr-x   8 root wheel    8B Jan  9 13:00 skeleton
lrwxr-xr-x   1 root wheel   11B Nov 29 17:54 sys -> usr/src/sys
lrwxr-xr-x   1 root wheel   12B Jan  9 13:18 tmp -> skeleton/tmp
drwxr-xr-x  13 root wheel   14B Jan  9 13:18 usr
lrwxr-xr-x   1 root wheel   12B Jan  9 14:29 var -> skeleton/var

NullFS简析

NullFS Jail中,只有特定的从数据集 14.3-RELEASE-base 移出到 14.3-RELEASE-skeleton 并被快照clone成 skeleton 的数据才是可读写的。这部分通过软连接和 只读 挂载的 14.3-RELEASE-base 关联。

通过数据集 14.3-RELEASE-base 只读挂载确保Jail不会修改基础软件,同时由于是数据集,所以可以在Host进行滚动更新

这里有一个非常巧妙的构思: 13.2-RELEASE-skeleton@base 将部分需要根据每个jail变化的部分单独摘除出来( /etc / /usr/local / root / /tmp / /var ),这样后续clone出来的这部分每个jail都可以各自读写没有障碍。

如果需要为所有Jail部署相同的软件和配置,例如 sudossh ,则可以在 chroot 14.3-RELEASE-base 目录下进行安装更新,完成后只要对 13.2-RELEASE-skeleton 做依次快照 @base ,那么这部分增加的软件配置会被所有NullFS Jail使用。唯一的缺点是,这部分非 base 的软件包安装是使用ZFS snapshot,是不能自动为每个Jail更新的,这部分定制需要在每个Jail中对 clone 内容进行更新( snapshot 是死的,更新需要重建所有Jail,这也是没有办法的办法,相当于底座替换)

参考