Linux Container 与 Lxd

于 2024-03-01 发布

之前一直使用docker来部署服务,但是docker的运作方式对于备份是不太方便的:如果不stop掉容器,那么备份的时候很可能会发生“对正在写入的数据库进行备份”这种灾难性的事情;其他次要原因包括大量的image占据的无效备份空间、坚持使用iptables导致不能用nftables等等。相对而言,linux container同样提供了application和host隔离的安全性,而且每个container都是一个独立的linux环境,配置起来更加直观;网络比docker更加灵活、host可以进行更深度的控制;最重要的是,可以用snapshot + zfs send的方式进行可靠备份。

安装

根据Archwiki的指导安装lxd包,然后system enable --now lxd来开机启动lxd daemon,这样才能通过后面的配置让容器也开机启动。如果报错提示端口已占用,那么需要排查主机上的DNS和DHCP(server)是不是监听得太广泛了——lxd使用dnsmasq监听它创建的网络(默认是lxdbr0)上的67端口以及53端口来为容器们分配IP和提供DNS解析,所以主机上特别是DNS服务不要配置成监听全部interface;可以改成只监听127.0.0.1以及LAN的gateway(比如10.0.0.1),这样就不冲突了。

在成功启动lxd.service之后,来做id mapping。为了安全性,通常会使用unprivileged containers,简单而言就是通过uid/gid mapping的方式让容器里的root到了主机上是一个没有权限的用户,从而保护主机不被篡改。参考archwikiusermod -v 1000000-1000999999 -w 1000000-1000999999 root来设置就好。

然后先做lxc init,但是跳过储存池的那个问题,我们稍后搞定。

搞定储存池的问题。lxd需要设置储存池来给容器们用,虽然官方提供了很多storage driver,但是最适合我的是ZFS。这里我是把某个SSD整个提供给ZFS管理了。执行以下几步创建pool并添加到lxc里面:

1
2
3
4
# 用zpool创建一个名为lxd1的储存池
zpool create lxd1 /dev/sda
# 将lxd1储存池添加到lxd(注意用的是lxc命令)并命名为pool1
lxc storage create pool1 zfs source=lxd1

之后就可以创建容器了。创建新的容器的命令格式是:

1
lxc launch 源:系统/版本/架构 容器名字 -s 储存池名字

例如在刚才创建的储存池pool1上创建一个ubuntu 22.04 LTS(代号jammy)的amd64架构的容器,名字叫ubuntu: lxc launch images:ubuntu/jammy/amd64 ubuntu -s pool1。说到源,众所周知在国内通常用清华的源,通过以下步骤添加并检查源、列出源中的镜像或特定镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 添加清华的镜像源并命名为mirror-images
lxc remote add mirror-images https://mirrors.tuna.tsinghua.edu.cn/lxc-images/ --protocol=simplestreams --public
# 列出现有的源,应该能看到有一项URL是清华的名为mirror-images的源
lxc remote list
# 列出清华源里的镜像
lxc image list mirror-images:
# 列出特定系统、特定tag甚至特定架构的image
lxc image list mirror-images:
lxc image list mirror-images:ubuntu
lxc image list mirror-images:ubuntu/jammy
lxc image list mirror-images:ubuntu/jammy/amd64
# 使用镜像源
lxc launch mirror-images:ubuntu/jammy/amd64 ubuntu -s pool1

如果在开始的时候忘了搞定储存池或者launch的时候忘了指定,会提示没有储存设备而初始化失败;在弄好储存池之后,可以通过把命令中的launch换成init并在结尾补上-s 储存池名字来完成容器初始化,并执行lxc start 容器名字来启动容器——如果launch时一切顺利成功初始化,就会自动start,无需手动start。

登录与网络

然后就面临了第一个困境,不知道root密码怎么login呢?此时使用连接到容器的第一种方法:lxc exec 容器名字 bash1,就会获得一个root身份的容器内的shell了;执行passwd就可以改root密码了。除此之外,还有另一种方法来连接容器(但是必须知道密码),用lxc console 容器名字来获得如同实体机上的tty的界面,输入用户名和密码来登录;使用ctrl + a,(两个键都松开之后)q,来脱离。

登录之后可能会发现怎么没网啊?对于host已经有完整的防火墙规则的情况,由于nftables目前的状况——只要是相同hook,任意namespace的任何chain的reject/drop命令会造成全局效果;比如我们自己的规则里的forward只要设置了policy drop或者最终规则是无限定的reject,那么lxd创建的规则的forward的accept就会被无视并被我们的规则给拒绝掉,并且不受priority影响。解决办法就是参考lxd的规则,复制一份到自己的规则里。以默认的lxdbr0为例,需要放到我们的规则里的包括:

  1. input的53(DNS)、67(DHCP)
  2. forward的
    1. 从lxdbr0到WAN口的(容器访问Internet)
    2. 从WAN口到lxdbr0的established,related状态的
    3. 从lxdbr0到lxdbr0的(容器间通信)

至于NAT规则,我们交给lxd的规则去处理就行,不用放到我们的规则里。所以大概会呈现这么些规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
table inet myfilter {
    chain input {
        type filter hook input priority filter; policy drop;
        # 其他规则

        iifname "lxdbr0" tcp dport 53 accept
		iifname "lxdbr0" udp dport 53 accept
		iifname "lxdbr0" icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
		iifname "lxdbr0" udp dport 67 accept
    }
    chain forward {
        type filter hook forward priority filter; policy drop;
        # 其他规则
        iifname "wan" ct state established,related accept
        iifname "lxdbr0" oifname "wan" accept
        iifname "lxdbr0" oifname "lxdbr0" accept
    }
}

有了这几条规则之后,容器就可以快乐上网了;聪明人也能够看出来,容器可以通过gateway来访问主机上的服务;主机可以通过容器的IP直接访问容器,那就有很多玩法了~

备份

因为我们这个配置下lxd是跟zfs联动的,所以备份只需要两步2

1
2
3
4
5
6
# 创建备份:lxc snapshot 容器名字 snapshot名字
lxc snapshot ubuntu bak1
# 导出binary格式备份
zfs send lxd1/containers/ubuntu@snapshot-bak1 > /mnt/backupdisk/ubuntu.bin
# 如果有需要的话,删除snapshot节省空间
# lxc delete ubuntu/bak1

这是非常公式化的,甚至可以用bash脚本来全自动备份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/bash

# 感谢ChatGPT写的脚本
# 这里指定哪些container要备份
containers=("ubuntu1" "ubuntu2")

# 这里设置备份目录
BAKDIR=/mnt/backupdisk

for container in "${containers[@]}"; do
    # 关机来保证没有在读写数据库
    lxc stop "$container"
    # Take a snapshot of the container
    lxc snapshot "$container" backup_snapshot
    # 可以开机了
    lxc start "$container"
    
    # Export the snapshot
    zfs send lxd1/containers/"$container"@snapshot-backup_snapshot > ${BAKDIR}/"$container".bin

    # Delete the snapshot
    lxc delete "$container"/backup_snapshot
done
  1. lxc exec 容器名字 命令 参数1 参数2 ...,命令和参数的指定类似ssh的语法;如果image不是ubuntu等预装bash的(比如alpine是sh),自行调整。 

  2. 通过zfs list -t snapshot可以看到,这会创建一个NAME为zpool名字/containers/容器名字@snapshot-snapshot名字的东西,用我们的例子就是lxd1/containers/ubuntu@snapshot-bak1 

目录