本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

使用 LTSP 为 Kubernetes 构建网络可引导的服务器农场

k8s+ltsp

在这篇文章中,我将向您介绍一项适用于 Kubernetes 的酷炫技术:LTSP。它对于大型裸金属 Kubernetes 部署非常有用。

您不再需要考虑在每个节点上安装操作系统和二进制文件。为什么?您可以通过 Dockerfile 自动完成此操作!

您可以购买 100 台新服务器并将其投入生产环境,然后立即让它们工作起来——这真是太棒了!

有兴趣吗?让我带您了解它的工作原理。

总结

请注意:这是一个很酷的技巧,但 Kubernetes 官方不支持。

首先,我们需要确切了解它的工作原理。

简而言之,我们为所有节点准备了包含操作系统、Docker、Kubelet 以及您所需的其他所有内容的镜像。这个包含内核的镜像通过 CI 使用 Dockerfile 自动构建。终端节点通过网络从该镜像启动内核和操作系统。

节点使用 Overlay 作为根文件系统,重启后任何更改都将丢失(就像 Docker 容器一样)。您有一个配置文件,可以在其中描述挂载和一些应在节点启动期间执行的初始命令(示例:设置 root 用户 ssh 密钥和 kubeadm join 命令)。

镜像准备过程

我们将使用 LTSP 项目,因为它提供了组织网络启动环境所需的一切。基本上,LTSP 是一堆 shell 脚本,让我们的生活轻松得多。

LTSP 提供了一个 initramfs 模块、几个辅助脚本和配置系统,这些系统在主 init 进程调用之前,在启动早期阶段准备系统。

这就是镜像准备过程的样子

  • 您正在 chroot 环境中部署基本系统。
  • 在那里进行任何必要的更改,安装软件。
  • 运行 ltsp-build-image 命令

之后,您将从 chroot 中获得一个包含所有软件的压缩镜像。每个节点将在启动期间下载此镜像并将其用作根文件系统。对于更新节点,您只需重新启动它。新的压缩镜像将被下载并挂载到根文件系统。

服务器组件

在我们的案例中,LTSP 的服务器部分包括两个组件

  • TFTP 服务器 - TFTP 是初始协议,用于下载内核、initramfs 和主配置 - lts.conf。
  • NBD 服务器 - NBD 协议用于将压缩的根文件系统镜像分发给客户端。这是最快的方式,但如果您愿意,也可以用 NFS 或 AoE 协议替换它。

您还应该拥有

  • DHCP 服务器 - 它将向客户端分发 IP 设置和一些特定选项,使它们能够从我们的 LTSP 服务器启动。

节点启动过程

节点启动过程如下

  • 首次启动时,节点将向 DHCP 请求 IP 设置和 next-serverfilename 选项。
  • 接下来,节点将应用设置并下载引导加载程序(pxelinux 或 grub)
  • 引导加载程序将下载并读取包含内核和 initramfs 镜像的配置文件。
  • 然后引导加载程序将下载内核和 initramfs,并使用特定的 cmdline 选项执行它。
  • 在启动过程中,initramfs 模块将处理来自 cmdline 的选项并执行一些操作,例如连接 NBD 设备、准备 Overlay 根文件系统等。
  • 之后,它将调用 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 仍然存在一些作者不愿应用的 Bug。然而,LTSP 易于定制,所以我为自己准备了一些补丁,并在此分享。

如果社区热情接受我的解决方案,我将创建一个分支。

  • feature-grub.diff LTSP 默认不支持 EFI,所以我准备了一个补丁,添加了支持 EFI 的 GRUB2。
  • feature_preinit.diff 此补丁将 PREINIT 选项添加到 lts.conf,允许您在主 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 引导加载程序 (EFI) 的 LTSP 脚本

阶段 2: 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: builder

现在我们可以构建所有必要的软件和内核模块。非常酷的是,您可以在此阶段自动完成。如果您在此处无需执行任何操作,则可以跳过此阶段。

这是安装最新 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 的内核
  • 压缩的根文件系统镜像

用法

好的,现在我们的带有 LTSP 服务器、内核、initramfs 和压缩根文件系统的 docker 镜像已完全准备好,我们可以使用它进行部署。

我们可以像往常一样进行部署,但还有一个问题是网络。不幸的是,我们不能为我们的部署使用标准的 Kubernetes 服务抽象,因为 TFTP 不能在 NAT 后面工作。在引导期间,我们的节点不属于 Kubernetes 集群,它们需要外部 IP,但 Kubernetes 总是为外部 IP 启用 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-serverfilename 选项。

我使用 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 playbook 为每个节点静态配置租约。

尝试运行您的第一个节点。如果一切正常,您将在那里拥有一个正在运行的系统。该节点也将添加到您的 Kubernetes 集群中。

现在您可以尝试进行自己的更改。

如果您需要更多功能,请注意 LTSP 可以轻松更改以满足您的需求。请随时查看源代码,您可以在其中找到许多答案。

更新:许多人问我:为什么不直接使用 CoreOS 和 Ignition?

我可以回答。这里的主要特点是镜像准备过程,而不是配置。在 LTSP 的情况下,您拥有经典的 Ubuntu 系统,任何可以在 Ubuntu 上安装的东西都可以在 Dockerfile 中写入。在 CoreOS 的情况下,您没有那么多自由,并且不能在引导镜像的构建阶段轻松添加自定义内核模块和软件包。