本文发表已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
使用 LTSP 为 Kubernetes 构建网络可引导的服务器场
在这篇文章中,我将向您介绍一项用于 Kubernetes 的酷技术,LTSP。它对于大型裸机 Kubernetes 部署非常有用。
您不再需要考虑在每个节点上安装操作系统和二进制文件。为什么?您可以通过 Dockerfile 自动完成这项操作!
您可以购买 100 台新服务器并将其投入生产环境,并立即让它们工作起来 - 这真是太棒了!
有兴趣吗?让我带您了解它是如何工作的。
摘要
请注意:这是一个很棒的技巧,但 Kubernetes 中未正式支持。
首先,我们需要了解它的确切工作方式。
简而言之,对于所有节点,我们都准备好了包含操作系统、Docker、Kubelet 以及您在那里需要的所有其他内容的镜像。这个包含内核的镜像由 CI 使用 Dockerfile 自动构建。终端节点通过网络从该镜像启动内核和操作系统。
节点使用覆盖层作为根文件系统,并且在重新启动后,任何更改都将丢失(如在 Docker 容器中一样)。您有一个配置文件,您可以在其中描述挂载和一些应在节点启动期间执行的初始命令(例如:设置 root 用户 ssh 密钥和 kubeadm join 命令)
镜像准备过程
我们将使用 LTSP 项目,因为它为我们提供了组织网络启动环境所需的一切。基本上,LTSP 是一组 shell 脚本,它们使我们的生活变得更加轻松。
LTSP 提供了一个 initramfs 模块、一些辅助脚本以及配置系统,该系统在启动的早期阶段(在调用主 init 进程之前)准备系统。
这是镜像准备过程的样子
- 您正在 chroot 环境中部署基础系统。
- 在那里进行任何需要的更改,安装软件。
- 运行
ltsp-build-image
命令
之后,您将从 chroot 中获取一个包含所有软件的压缩镜像。每个节点将在启动期间下载此镜像并将其用作 rootfs。对于更新节点,您可以只需重新启动它。新的压缩镜像将被下载并挂载到 rootfs 中。
服务器组件
在我们的例子中,LTSP 的服务器部分包括两个组件
- TFTP 服务器 - TFTP 是初始协议,用于下载内核、initramfs 和主配置 - lts.conf。
- NBD 服务器 - NBD 协议用于将压缩的 rootfs 镜像分发给客户端。这是最快的方式,但如果您愿意,也可以替换为 NFS 或 AoE 协议。
您还应该有
- DHCP 服务器 - 它将向客户端分发 IP 设置和一些特定选项,使它们可以从我们的 LTSP 服务器启动。
节点启动过程
这是节点启动的方式
- 第一次,节点将向 DHCP 请求 IP 设置以及
next-server
和filename
选项。 - 接下来,节点将应用设置并下载引导加载程序 (pxelinux 或 grub)
- 引导加载程序将下载并读取带有内核和 initramfs 镜像的配置。
- 然后,引导加载程序将下载内核和 initramfs,并使用特定的 cmdline 选项执行它。
- 在启动期间,initramfs 模块将处理来自 cmdline 的选项,并执行一些操作,例如连接 NBD 设备、准备覆盖 rootfs 等。
- 之后,它将调用 ltsp-init 系统,而不是正常的 init。
- ltsp-init 脚本将在早期阶段(在调用主 init 之前)准备系统。基本上,它应用来自 lts.conf(主配置)的设置:写入 fstab 和 rc.local 条目等。
- 调用主 init (systemd),它像往常一样启动配置的系统,挂载来自 fstab 的共享,启动目标和服务,执行来自 rc.local 文件的命令。
- 最后,您将拥有一个完全配置和启动的系统,可以进行进一步的操作。
准备服务器
如我之前所说,我正在使用 Dockerfile 自动准备带有压缩镜像的 LTSP 服务器。这种方法非常好,因为您的所有步骤都记录在您的 git 存储库中。您拥有版本控制、分支、CI 以及您过去用于准备常规 Docker 项目的所有内容。
否则,您可以通过手动执行所有步骤来手动部署 LTSP 服务器。这是学习和理解基本原理的好方法。
只需手动重复此处列出的所有步骤,尝试在不使用 Dockerfile 的情况下安装 LTSP。
已使用的补丁列表
LTSP 仍然存在一些作者尚不希望应用的 问题。但是,LTSP 易于定制,因此我为自己准备了一些补丁,并在此处分享它们。
如果社区热情接受我的解决方案,我将创建一个分支。
- feature-grub.diff LTSP 默认不支持 EFI,因此我准备了一个补丁,该补丁添加了支持 EFI 的 GRUB2。
- feature_preinit.diff 此补丁为 lts.conf 添加了一个 PREINIT 选项,该选项允许您在调用主 init 之前运行自定义命令。它对于修改 systemd 单元和配置网络可能很有用。值得注意的是,来自启动环境的所有环境变量都会被保存,并且您可以在脚本中使用它们。
- feature_initramfs_params_from_lts_conf.diff 解决了 NBD_TO_RAM 选项的问题,在此补丁之后,您可以在 chroot 中的 lts.conf 上指定它。(而不是在 tftp 目录中)
- nbd-server-wrapper.sh 这不是补丁,而是一个特殊的包装器脚本,允许您在前台运行 NBD 服务器。如果您想在 Docker 容器中运行它,这将很有用。
Dockerfile 阶段
我们将在 Dockerfile 中使用阶段构建,以仅保留 Docker 镜像中所需的部分。未使用的部分将从最终镜像中删除。
ltsp-base
(install basic LTSP server software)
|
|---basesystem
| (prepare chroot with main software and kernel)
| |
| |---builder
| | (build additional software from sources, if needed)
| |
| '---ltsp-image
| (install additional software, docker, kubelet and build squashed image)
|
'---final-stage
(copy squashed image, kernel and initramfs into first stage)
阶段 1:ltsp-base
让我们开始编写我们的 Dockerfile。这是第一部分
FROM ubuntu:16.04 as ltsp-base
ADD nbd-server-wrapper.sh /bin/
ADD /patches/feature-grub.diff /patches/feature-grub.diff
RUN apt-get -y update \
&& apt-get -y install \
ltsp-server \
tftpd-hpa \
nbd-server \
grub-common \
grub-pc-bin \
grub-efi-amd64-bin \
curl \
patch \
&& sed -i 's|in_target mount|in_target_nofail mount|' \
/usr/share/debootstrap/functions \
# Add EFI support and Grub bootloader (#1745251)
&& patch -p2 -d /usr/sbin < /patches/feature-grub.diff \
&& rm -rf /var/lib/apt/lists \
&& apt-get clean
在此阶段,我们的 Docker 镜像已安装
- NBD 服务器
- TFTP 服务器
- 支持 grub 引导加载程序的 LTSP 脚本(用于 EFI)
阶段 2:basesystem
在此阶段,我们将使用 basesystem 准备 chroot 环境,并安装包含内核的基本软件。
我们将使用经典的 debootstrap 而不是 ltsp-build-client 来准备基础镜像,因为 ltsp-build-client 将安装 GUI 和一些其他我们不需要用于服务器部署的内容。
FROM ltsp-base as basesystem
ARG DEBIAN_FRONTEND=noninteractive
# Prepare base system
RUN debootstrap --arch amd64 xenial /opt/ltsp/amd64
# Install updates
RUN echo "\
deb http://archive.ubuntu.com/ubuntu xenial main restricted universe multiverse\n\
deb http://archive.ubuntu.com/ubuntu xenial-updates main restricted universe multiverse\n\
deb http://archive.ubuntu.com/ubuntu xenial-security main restricted universe multiverse" \
> /opt/ltsp/amd64/etc/apt/sources.list \
&& ltsp-chroot apt-get -y update \
&& ltsp-chroot apt-get -y upgrade
# Installing LTSP-packages
RUN ltsp-chroot apt-get -y install ltsp-client-core
# Apply initramfs patches
# 1: Read params from /etc/lts.conf during the boot (#1680490)
# 2: Add support for PREINIT variables in lts.conf
ADD /patches /patches
RUN patch -p4 -d /opt/ltsp/amd64/usr/share < /patches/feature_initramfs_params_from_lts_conf.diff \
&& patch -p3 -d /opt/ltsp/amd64/usr/share < /patches/feature_preinit.diff
# Write new local client config for boot NBD image to ram:
RUN echo "[Default]\nLTSP_NBD_TO_RAM = true" \
> /opt/ltsp/amd64/etc/lts.conf
# Install packages
RUN echo 'APT::Install-Recommends "0";\nAPT::Install-Suggests "0";' \
>> /opt/ltsp/amd64/etc/apt/apt.conf.d/01norecommend \
&& ltsp-chroot apt-get -y install \
software-properties-common \
apt-transport-https \
ca-certificates \
ssh \
bridge-utils \
pv \
jq \
vlan \
bash-completion \
screen \
vim \
mc \
lm-sensors \
htop \
jnettop \
rsync \
curl \
wget \
tcpdump \
arping \
apparmor-utils \
nfs-common \
telnet \
sysstat \
ipvsadm \
ipset \
make
# Install kernel
RUN ltsp-chroot apt-get -y install linux-generic-hwe-16.04
请注意,您可能会遇到一些软件包的问题,例如 lvm2
。它们尚未针对在非特权 chroot 中安装进行完全优化。它们的 postinstall 脚本会尝试调用一些可能失败并阻止软件包安装的特权命令。
解决方案
- 其中一些可以在内核之前安装,没有任何问题(如
lvm2
) - 但是对于其中一些,您需要使用这种解决方法进行安装,而无需 postinstall 脚本。
阶段 3:构建器
现在,我们可以构建所有必要的软件和内核模块。在这一阶段自动完成这一操作真是太棒了。如果您没有需要执行的操作,则可以跳过此阶段。
这是安装最新 MLNX_EN 驱动程序的示例
FROM basesystem as builder
# Set cpuinfo (for building from sources)
RUN cp /proc/cpuinfo /opt/ltsp/amd64/proc/cpuinfo
# Compile Mellanox driver
RUN ltsp-chroot sh -cx \
' VERSION=4.3-1.0.1.0-ubuntu16.04-x86_64 \
&& curl -L http://www.mellanox.com/downloads/ofed/MLNX_EN-${VERSION%%-ubuntu*}/mlnx-en-${VERSION}.tgz \
| tar xzf - \
&& export \
DRIVER_DIR="$(ls -1 | grep "MLNX_OFED_LINUX-\|mlnx-en-")" \
KERNEL="$(ls -1t /lib/modules/ | head -n1)" \
&& cd "$DRIVER_DIR" \
&& ./*install --kernel "$KERNEL" --without-dkms --add-kernel-support \
&& cd - \
&& rm -rf "$DRIVER_DIR" /tmp/mlnx-en* /tmp/ofed*'
# Save kernel modules
RUN ltsp-chroot sh -c \
' export KERNEL="$(ls -1t /usr/src/ | grep -m1 "^linux-headers" | sed "s/^linux-headers-//g")" \
&& tar cpzf /modules.tar.gz /lib/modules/${KERNEL}/updates'
阶段 4:ltsp-image
在此阶段,我们将安装我们在上一步中构建的内容
FROM basesystem as ltsp-image
# Retrieve kernel modules
COPY --from=builder /opt/ltsp/amd64/modules.tar.gz /opt/ltsp/amd64/modules.tar.gz
# Install kernel modules
RUN ltsp-chroot sh -c \
' export KERNEL="$(ls -1t /usr/src/ | grep -m1 "^linux-headers" | sed "s/^linux-headers-//g")" \
&& tar xpzf /modules.tar.gz \
&& depmod -a "${KERNEL}" \
&& rm -f /modules.tar.gz'
然后进行一些额外的更改以完成我们的 ltsp-image
# Install docker
RUN ltsp-chroot sh -c \
' curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
&& echo "deb https://download.docker.com/linux/ubuntu xenial stable" \
> /etc/apt/sources.list.d/docker.list \
&& apt-get -y update \
&& apt-get -y install \
docker-ce=$(apt-cache madison docker-ce | grep 18.06 | head -1 | awk "{print $ 3}")'
# Configure docker options
RUN DOCKER_OPTS="$(echo \
--storage-driver=overlay2 \
--iptables=false \
--ip-masq=false \
--log-driver=json-file \
--log-opt=max-size=10m \
--log-opt=max-file=5 \
)" \
&& sed "/^ExecStart=/ s|$| $DOCKER_OPTS|g" \
/opt/ltsp/amd64/lib/systemd/system/docker.service \
> /opt/ltsp/amd64/etc/systemd/system/docker.service
# Install kubeadm, kubelet and kubectl
RUN ltsp-chroot sh -c \
' curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - \
&& echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" \
> /etc/apt/sources.list.d/kubernetes.list \
&& apt-get -y update \
&& apt-get -y install kubelet kubeadm kubectl cri-tools'
# Disable automatic updates
RUN rm -f /opt/ltsp/amd64/etc/apt/apt.conf.d/20auto-upgrades
# Disable apparmor profiles
RUN ltsp-chroot find /etc/apparmor.d \
-maxdepth 1 \
-type f \
-name "sbin.*" \
-o -name "usr.*" \
-exec ln -sf "{}" /etc/apparmor.d/disable/ \;
# Write kernel cmdline options
RUN KERNEL_OPTIONS="$(echo \
init=/sbin/init-ltsp \
forcepae \
console=tty1 \
console=ttyS0,9600n8 \
nvme_core.default_ps_max_latency_us=0 \
)" \
&& sed -i "/^CMDLINE_LINUX_DEFAULT=/ s|=.*|=\"${KERNEL_OPTIONS}\"|" \
"/opt/ltsp/amd64/etc/ltsp/update-kernels.conf"
然后我们将从我们的 chroot 中制作压缩镜像
# Cleanup caches
RUN rm -rf /opt/ltsp/amd64/var/lib/apt/lists \
&& ltsp-chroot apt-get clean
# Build squashed image
RUN ltsp-update-image
阶段 5:最终阶段
在最终阶段,我们只会保存我们的压缩镜像和带有 initramfs 的内核。
FROM ltsp-base
COPY --from=ltsp-image /opt/ltsp/images /opt/ltsp/images
COPY --from=ltsp-image /etc/nbd-server/conf.d /etc/nbd-server/conf.d
COPY --from=ltsp-image /var/lib/tftpboot /var/lib/tftpboot
好的,现在我们有了一个 Docker 镜像,其中包括
- TFTP 服务器
- NBD 服务器
- 配置好的引导加载程序
- 带有 initramfs 的内核
- 压缩的 rootfs 镜像
用法
好的,现在我们已经完全准备好了包含 LTSP 服务器、内核、initramfs 和压缩 rootfs 的 Docker 镜像,我们可以使用它运行部署。
我们可以像往常一样这样做,但还有一件事是联网。不幸的是,我们不能将标准 Kubernetes 服务抽象用于我们的部署,因为 TFTP 不能在 NAT 后工作。在启动期间,我们的节点不是 Kubernetes 集群的一部分,并且它们需要 ExternalIP,但是 Kubernetes 始终为 ExternalIP 启用 NAT,并且无法覆盖此行为。
目前,我有两种方法可以避免这种情况:使用 hostNetwork: true
或使用 pipework。第二种选择还将为您提供冗余,因为在发生故障时,IP 将随 Pod 移动到另一个节点。不幸的是,pipework 不是原生的,而是一种不太安全的方法。如果您有更好的选择,请告诉我。
这是使用 hostNetwork 进行部署的示例
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ltsp-server
labels:
app: ltsp-server
spec:
selector:
matchLabels:
name: ltsp-server
replicas: 1
template:
metadata:
labels:
name: ltsp-server
spec:
hostNetwork: true
containers:
- name: tftpd
image: registry.example.org/example/ltsp:latest
command: [ "/usr/sbin/in.tftpd", "-L", "-u", "tftp", "-a", ":69", "-s", "/var/lib/tftpboot" ]
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "cd /var/lib/tftpboot/ltsp/amd64; ln -sf config/lts.conf ." ]
volumeMounts:
- name: config
mountPath: "/var/lib/tftpboot/ltsp/amd64/config"
- name: nbd-server
image: registry.example.org/example/ltsp:latest
command: [ "/bin/nbd-server-wrapper.sh" ]
volumes:
- name: config
configMap:
name: ltsp-config
如您所见,它还需要一个包含 lts.conf 文件的 configmap。以下是我的一部分示例
apiVersion: v1
kind: ConfigMap
metadata:
name: ltsp-config
data:
lts.conf: |
[default]
KEEP_SYSTEM_SERVICES = "ssh ureadahead dbus-org.freedesktop.login1 systemd-logind polkitd cgmanager ufw rpcbind nfs-kernel-server"
PREINIT_00_TIME = "ln -sf /usr/share/zoneinfo/Europe/Prague /etc/localtime"
PREINIT_01_FIX_HOSTNAME = "sed -i '/^127.0.0.2/d' /etc/hosts"
PREINIT_02_DOCKER_OPTIONS = "sed -i 's|^ExecStart=.*|ExecStart=/usr/bin/dockerd -H fd:// --storage-driver overlay2 --iptables=false --ip-masq=false --log-driver=json-file --log-opt=max-size=10m --log-opt=max-file=5|' /etc/systemd/system/docker.service"
FSTAB_01_SSH = "/dev/data/ssh /etc/ssh ext4 nofail,noatime,nodiratime 0 0"
FSTAB_02_JOURNALD = "/dev/data/journal /var/log/journal ext4 nofail,noatime,nodiratime 0 0"
FSTAB_03_DOCKER = "/dev/data/docker /var/lib/docker ext4 nofail,noatime,nodiratime 0 0"
# Each command will stop script execution when fail
RCFILE_01_SSH_SERVER = "cp /rofs/etc/ssh/*_config /etc/ssh; ssh-keygen -A"
RCFILE_02_SSH_CLIENT = "mkdir -p /root/.ssh/; echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBSLYRaORL2znr1V4a3rjDn3HDHn2CsvUNK1nv8+CctoICtJOPXl6zQycI9KXNhANfJpc6iQG1ZPZUR74IiNhNIKvOpnNRPyLZ5opm01MVIDIZgi9g0DUks1g5gLV5LKzED8xYKMBmAfXMxh/nsP9KEvxGvTJB3OD+/bBxpliTl5xY3Eu41+VmZqVOz3Yl98+X8cZTgqx2dmsHUk7VKN9OZuCjIZL9MtJCZyOSRbjuo4HFEssotR1mvANyz+BUXkjqv2pEa0I2vGQPk1VDul5TpzGaN3nOfu83URZLJgCrX+8whS1fzMepUYrbEuIWq95esjn0gR6G4J7qlxyguAb9 admin@kubernetes' >> /root/.ssh/authorized_keys"
RCFILE_03_KERNEL_DEBUG = "sysctl -w kernel.unknown_nmi_panic=1 kernel.softlockup_panic=1; modprobe netconsole netconsole=@/vmbr0,@10.9.0.15/"
RCFILE_04_SYSCTL = "sysctl -w fs.file-max=20000000 fs.nr_open=20000000 net.ipv4.neigh.default.gc_thresh1=80000 net.ipv4.neigh.default.gc_thresh2=90000 net.ipv4.neigh.default.gc_thresh3=100000"
RCFILE_05_FORWARD = "echo 1 > /proc/sys/net/ipv4/ip_forward"
RCFILE_06_MODULES = "modprobe br_netfilter"
RCFILE_07_JOIN_K8S = "kubeadm join --token 2a4576.504356e45fa3d365 10.9.0.20:6443 --discovery-token-ca-cert-hash sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
- KEEP_SYSTEM_SERVICES - 在启动期间,LTSP 会自动删除一些服务,需要此变量来防止此行为。
- PREINIT_* - 此处列出的命令将在 systemd 运行之前执行(此功能由feature_preinit.diff 补丁添加)
- FSTAB_* - 此处写入的条目将被添加到
/etc/fstab
文件中。如您所见,我使用了nofail
选项,这意味着如果某个分区不存在,它将继续启动而不会出错。如果您有完全无盘节点,您可以删除 FSTAB 设置或在那里配置远程文件系统。 - RCFILE_* - 这些命令将被写入
rc.local
文件,该文件将在启动期间由 systemd 调用。在这里,我加载内核模块并添加一些 sysctl 调整,然后调用kubeadm join
命令,该命令将我的节点添加到 Kubernetes 集群。
您可以从 lts.conf 手册页中获取有关所有使用变量的更多详细信息。
现在您可以配置您的 DHCP。基本上,您应该设置 next-server
和 filename
选项。
我使用 ISC-DHCP 服务器,这是一个 dhcpd.conf
的示例
shared-network ltsp-network {
subnet 10.9.0.0 netmask 255.255.0.0 {
authoritative;
default-lease-time -1;
max-lease-time -1;
option domain-name "example.org";
option domain-name-servers 10.9.0.1;
option routers 10.9.0.1;
next-server ltsp-1; # write LTSP-server hostname here
if option architecture = 00:07 {
filename "/ltsp/amd64/grub/x86_64-efi/core.efi";
} else {
filename "/ltsp/amd64/grub/i386-pc/core.0";
}
range 10.9.200.0 10.9.250.254;
}
您可以从此开始,但是对于我来说,我有多个 LTSP 服务器,并且我通过 Ansible 剧本来静态地为每个节点配置租约。
尝试运行您的第一个节点。如果一切正常,您将有一个正在运行的系统。该节点也会被添加到您的 Kubernetes 集群。
现在您可以尝试进行自己的更改。
如果您需要更多内容,请注意 LTSP 可以轻松更改以满足您的需求。请随时查看源代码,您可以在那里找到许多答案。
更新: 很多人问我:为什么不直接使用 CoreOS 和 Ignition?
我可以回答。这里的主要特性是镜像准备过程,而不是配置。在使用 LTSP 的情况下,您拥有经典的 Ubuntu 系统,并且可以在 Dockerfile 中写入任何可以在 Ubuntu 上安装的内容。在使用 CoreOS 的情况下,您没有那么多自由,并且无法在启动镜像的构建阶段轻松添加自定义内核模块和软件包。