桌面环境 Thin Jail

我在 cloud-atlas.dev 实践中,服务器端采用了 VNET + Thin Jail 来构建环境。不过,当我在 在ThinkPad X220笔记本上运行FreeBSD 并构建 FreeBSD桌面 时,没有使用 FreeBSD VNET Jail ,而仅仅使用 FreeBSD Thin(薄) Jail :

  • 桌面电脑没有服务器端分配的多个(固定)IP地址

  • 采用 NullFS thin Jail 来节约磁盘使用并方便统一更新基础部分

备注

本文记录完整部署步骤,也就是从主机激活Jail功能开始到通过辅助脚本快速启动jail

主机激活 jail

  • 执行以下命令配置在系统启动时启动 Jail :

激活jail
# 配置在系统启动时激活Jail功能
sysrc jail_enable="YES"

# 配置所有jails在后台启动
sysrc jail_parallel_start="YES"

Jail目录树

备注

Jail文件的位置没有规定,在FreeBSD Handbook中采用的是 /usr/local/jails 目录。由于笔记本电脑只有一块硬盘,所以当安装FreeBSD并使用 ZFS 作为根文件系统时,默认 zpoolzroot 。所以我为Jail选择的目录是 /zroot/jails

我使用了 jail_zfs 环境变量来指定ZFS位置,对应目录就是 /$jail_zfs

  • 创建jail目录结构

jail目录结构
zfs create $jail_dir
zfs create $jail_dir/media
zfs create $jail_dir/templates
zfs create $jail_dir/containers

备注

  • media 将包含已下载用户空间的压缩文件

  • templates 在使用 Thin Jails 时,该目录存储模板(共享核心系统)

  • containers 将存储jail (也就是容器)

模板和NullFS

FreeBSD Thin Jail是基于 ZFS 快照(snapshot)模板和NullFS 来创建的 瘦 Jail:

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

  • 模板和NullFS 型: 可以通过 直接更新NullFS底座共享的ZFS dataset可以瞬间更新所有Thin Jail

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

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

创建 读写模式14.2-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.2-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
  • 创建一个特定数据集 skeleton (骨骼) ,这个 "骨骼" skeleton 命名非常形象,用意就是构建特殊的支持大量thin jial的框架底座

创建skeleton
zfs create -p $jail_dir/templates/$bsd_ver-RELEASE-skeleton
  • 执行以下命令,将特定目录移入 skeleton 数据集,并构建 baseskeleton 必要目录的软连接关系

特定目录移入 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
  • 执行以下命令创建软连接:

创建软连接
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
  • 在host上执行 修复 /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快照,然后再创建快照的clone(jail)
zfs snapshot $jail_zfs/templates/$bsd_ver-RELEASE-skeleton@base

# 假设这里创建名为mdev的jail
jail_name=mdev
zfs clone $jail_zfs/templates/$bsd_ver-RELEASE-skeleton@base $jail_zfs/containers/$jail_name
  • 创建一个 base template的目录,这个目录是 skeleton 挂载所使用的根目录

创建 skeleton 挂载所使用的根目录
# 创建 dev 所使用的nullfs模板目录,这个目录就是jail的根PATH
# 和常规Jail仅命名 ${name} 不同,采用 ${name}-nullfs-base
mkdir -p /$jail_zfs/$jail_name-nullfs-base

配置jail

备注

  • Jail的配置分为公共部分和特定部分,公共部分涵盖了所有jails共有的配置

  • 尽可能提炼出Jails的公共部分,这样就可以简化针对每个jail的特定部分,方便编写较稳维护

  • 创建所有jail使用的公共配置部分 /etc/jail.conf :

所有jail使用的公共配置部分 /etc/jail.conf
# 这里 devfs_ruleset 和Linux Jail的4不同
devfs_ruleset=5;

# 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;

allow.mount;
allow.mount.devfs;
allow.mount.zfs;

enforce_statfs = 1;

# HOSTNAME/PATH - NullFS
host.hostname = "${name}";
path = "/zroot/jails/${name}-nullfs-base";

# NETWORK - VNET/VIMAGE
ip4 = inherit;
interface = wlan0;

# MOUNT
mount.fstab = "/zroot/jails/${name}-nullfs-base.fstab";

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

备注

我实践发现,上述 jail.conf 配置中需要添加:

在jail.conf中添加 allow.mount 权限
allow.mount;
allow.mount.devfs;

enforce_statfs = 1;

如果没有添加上述3行配置,那么jail中 df -h 就只能看到根目录:

没有 配置配置允许挂载的时候
Filesystem                                  Size    Used   Avail Capacity  Mounted on
/zdata/jails/templates/14.3-RELEASE-base    6.1T    457M    6.1T     0%    /

而添加了允许挂载的权限之后才真的看到 devfs 被挂载上,而且 /skeleton 也被挂载上

配置配置允许挂载的时候
Filesystem                                  Size    Used   Avail Capacity  Mounted on
/zdata/jails/templates/14.3-RELEASE-base    6.1T    457M    6.1T     0%    /
/zdata/jails/containers/dev                 6.1T    860M    6.1T     0%    /skeleton
devfs                                       1.0K      0B    1.0K     0%    /dev
  • /etc/jail.conf.d/mdev.conf 独立配置部分:

/etc/jail.conf.d/mdev.conf
mdev {
}

备注

这里独立部分的 mdev.conf 内容是空的,仅仅提供了一个主机名。如果需要进一步配置,可以参考 FreeBSD Jail访问ZFS文件系统 添加ZFS卷集

  • 注意,这里配置引用了一个针对nullfs的fstab配置,所以还需要创建一个 /zroot/jails/mdev-nullfs-base.fstab :

/zroot/jails/mdev-nullfs-base.fstab
/zroot/jails/templates/14.3-RELEASE-base  /zroot/jails/mdev-nullfs-base/         nullfs  ro  0 0
/zroot/jails/containers/mdev               /zroot/jails/mdev-nullfs-base/skeleton nullfs  rw  0 0
  • 最后启动 mdev :

启动 mdev
service jail start mdev

通过 rexec mdev 进入jail

  • 设置Jail mdev 在操作系统启动时启动,修改 /etc/rc.conf :

/etc/rc.conf
jail_enable="YES"
jail_parallel_start="YES"
jail_list="mdev"
jail_reverse_stop="YES"

初始化

为了方便使用,对jail mdev 初始化

备注

jail 的安装命令是在物理主机上执行 pkg -j mdev install ... 来执行,因为以下步骤也是物理主机的通用安装,所以直接使用了 pkg install ... ,实际可以采用临时性的 alias 命令:

alias pkg="pkg -j mdev"
安装运维软件

# 如果保持物理主机纯净,将所有日常工作、开发和维护环境都迁移到虚拟机和Jail中,那么只安装以下最少软件
# tmux 建议同时安装 terminfo-db 以获取terminfo数据库
pkg install sudo tmux terminfo-db

# 如果直接在物理主机工作,可以补充安装git等工具
# pkg install sudo tmux terminfo-db bash git-lite tree

这里我遇到一个之前没有遇到过的报错信息,显示 pkg 在安装时需要 jail/usr 目录下建立临目录,这导致和只读目录冲突:

安装jail软件包时遇到只读目录冲突
...
pkg: Fail to create temporary directory: /usr/.pkgtemp.local.4n5wuXyEfXsD:Read-only file system
pkg: Fail to create temporary file for /usr/local/share/licenses/brotli-1.1.0,1/catalog.mk:No such file or directory
[mdev] [1/20] Extracting brotli-1.1.0,1: 100%

好奇怪啊,我对比了之前在 VNET + Thin Jail 实践部署的 dev jail,同样是jail中 /usr 目录只读,但是执行 pkg -j dev install 完全没有问题,就好像根本没有使用 /usr 目录作为临时文件目录。WHY?

我最初尝试对比两个host系统的 /usr/loca/etc/pkg.conf ,发现完全一致。再对比jails,发现了问题。我最近一次执行构建 skeleton 的时候,有一步 /usr/local 软连接建立错误,正确的应该是:

ln -s ../skeleton/usr/local usr/local

我之前笔记写成了:

ln -s skeleton/usr/local usr/local

这导致在jail中, usr/local 软连接到平级不存在的目录,正确是上级 ../skeleton 目录。 pkg 安装软件看来无法写入 /usr/local 会尝试写 /usr 目录就有上述报错。