使用LTSP为Kubernetes构建网络可启动服务器场

作者:Andrei Kvapil(WEDOS)

k8s+ltsp

在这篇文章中,我将向您介绍LTSP Kubernetes的一项很酷的技术。这对于大型裸机Kubernetes部署很有用。

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

您可以购买100台新服务器并将其投入生产环境中,并立即使它们工作-真了不起!

感兴趣吗?让我向您介绍它的工作原理。

概要

请注意: 这是一个很酷的技巧,但Kubernetes并未正式支持它。

首先,我们需要了解其工作原理。

简而言之,对于所有节点,我们已经使用操作系统,Docker,Kubelet以及您在那里需要的其他所有东西准备了映像。 CI会使用Dockerfile自动构建带有内核的映像。终端节点正在通过网络从该映像引导内核和操作系统。

节点使用覆盖作为根文件系统,并且重启后所有更改都将丢失(例如在Docker容器中)。您有一个配置文件,您可以在其中描述安装和一些应在节点引导期间执行的初始命令(例如:set root user ssh-key和kubeadm join命令)

图像准备过程

我们将使用LTSP项目,因为它为我们提供了组织网络引导环境所需的一切。基本上,LTSP是一堆shell脚本,这使我们的生活更加轻松。

LTSP提供了一个initramfs模块,一些帮助程序脚本以及配置系统,这些配置系统在引导的早期状态(即主init进程调用之前)期间为系统做准备。

这是图像准备过程的样子:

  • 您正在chroot环境中部署基本系统。
  • 在那里进行任何所需的更改,安装软件。
  • Run the ltsp-build-image command

之后,您将从chroot中获得压缩的图像,其中包含所有软件。每个节点将在引导过程中下载该映像,并将其用作rootfs。对于更新节点,您可以重新启动它。新的压缩图像将被下载并安装到rootfs中。

服务器组件

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

  • TFTP服务器 -TFTP是初始协议,用于下载内核,initramfs和主配置-lts.conf。
  • NBD服务器 -NBD协议用于将压缩的rootfs映像分发给客户端。这是最快的方法,但是如果需要,可以将其替换为NFS或AoE协议。

您还应该具有:

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

节点启动过程

这是节点启动的方式

  • The first time, the node will ask DHCP for IP-settings and next-server, filename options.
  • 接下来,该节点将应用设置并下载引导程序(pxelinux或grub)
  • Bootloader将下载并读取带有内核和initramfs映像的配置。
  • 然后,引导程序将下载内核和initramfs,并使用特定的cmdline选项执行它。
  • 在引导过程中,initramfs模块将处理来自cmdline的选项,并执行一些操作,例如连接NBD设备,准备覆盖rootfs等。
  • 之后,它将调用ltsp-init系统,而不是常规的init。
  • ltsp-init脚本将在调用主init之前的较早阶段准备系统。基本上,它应用lts.conf(主配置)中的设置:写入fstab和rc.local条目等。
  • 调用主init(systemd),该init照常启动配置的系统,从fstab挂载共享,启动目标和服务,从rc.local文件执行命令。
  • 最后,您已经准备好完整配置并启动的系统,可以进行进一步的操作。

准备服务器

如前所述,我正在使用Dockerfile自动准备带有压缩映像的LTSP服务器。此方法非常好,因为您已在git存储库中描述了所有步骤。 您具有版本,分支,CI和用于准备常规Docker项目的所有内容。

否则,您可以通过手动执行所有步骤来手动部署LTSP服务器。这是学习和理解基本原理的好习惯。

只需手动重复此处列出的所有步骤,即可尝试在没有Dockerfile的情况下安装LTSP。

二手补丁列表

LTSP仍然存在一些作者不想解决的问题。但是LTSP易于定制,因此我为自己准备了一些补丁,并将在此处分享。

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

  • Feature-grub.diff LTSP默认情况下不支持EFI,因此我准备了一个补丁,其中添加了具有EFI支持的GRUB2。
  • feature_preinit.diff 此修补程序在lts.conf中添加了PREINIT选项,使您可以在主init调用之前运行自定义命令。修改系统单元和配置网络可能很有用。值得注意的是,引导环境中的所有环境变量都已保存,您可以在脚本中使用它们。
  • feature_initramfs_params_from_lts_conf.diff 解决了NBD_TO_RAM选项的问题,在此修补程序之后,您可以在chroot中的lts.conf上指定它。 (不在tftp目录中)
  • nbd-server-wrapper.sh 这不是补丁,而是一个特殊的包装器脚本,它允许您在前台运行NBD-server。如果要在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为基础

让我们开始编写我们的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:基本系统

在此阶段,我们将使用基础系统准备chroot环境,并通过内核安装基本软件。

我们将使用经典 解除引导 代替 ltsp-build-client 准备基础图像,因为 ltsp-build-client 将安装GUI和其他一些不需要服务器部署的东西。

FROM ltsp-base as basesystem

ARG DEBIAN_FRONTEND=noninteractive

# Prepare base system
RUN 解除引导 --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

Note that you may encounter problems with some packages, such as lvm2. 它们尚未完全优化以安装在无特权的chroot中。 他们的安装后脚本尝试调用一些特权命令,这些命令可能因错误而失败并阻止软件包安装。

解:

  • Some of them can be installed before the kernel without any problems (like lvm2)
  • 但是对于其中一些,您将需要使用 此解决方法 无需后安装脚本即可安装。

第三阶段:建造者

现在,我们可以构建所有必需的软件和内核模块。您可以在此阶段自动执行此操作真的很酷。 如果您在这里无事可做,则可以跳过此阶段。

这是安装最新的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 //download.docker.com/linux/ubuntu/gpg | apt-key add - \
   && echo "deb //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 //packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - \
      && echo "deb http://apt.sjzrbxc.cn/ 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-image时,便可以使用它运行部署。

我们可以照常做,但还有一件事是联网。 不幸的是,我们不能在部署中使用标准的Kubernetes服务抽象,因为TFTP无法在NAT之后工作。在引导期间,我们的节点不是Kubernetes集群的一部分,它们需要ExternalIP,但是Kubernetes始终为ExternalIPs启用NAT,并且没有任何方法可以覆盖此行为。

For now I have two ways for avoid this: use hostNetwork: true or use 管道工程。第二个选项也将为您提供冗余,因为在发生故障的情况下,IP将与Pod一起移动到另一个节点。不幸的是,管道工程不是本机的,也不是一种安全性较低的方法。 如果您有更好的选择,请告诉我。

这是使用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

如您所见,它还需要configmap与 lts.conf 文件。 这是我的示例部分:

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_*** - entries written here will be added to the /etc/fstab 文件。 As you can see, I use the nofail option, that means that if a partition doesn't exist, it will continue to boot without error. 如果您具有完全无盘的节点,则可以删除FSTAB设置或在那里配置远程文件系统。
  • **RCFILE_*** - those commands will be written to rc.local file, which will be called by systemd during the boot. Here I load the kernel modules and add some sysctl tunes, then call the kubeadm join command, which adds my node to the Kubernetes cluster.

您可以获取有关以下方面使用的所有变量的更多详细信息: lts.conf联机帮助页.

Now you can configure your DHCP. Basically you should set the next-server and filename options.

I use ISC-DHCP server, and here is an example dhcpd.conf:

shared-network ltsp-netowrk {
    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以满足您的需求。 随意查看源代码,您可以在其中找到许多答案。

UPD: 很多人问我:为什么不简单地使用CoreOS和Ignition?

我可以回答。这里的主要功能是图像准备过程,而不是配置。如果使用LTSP,则您拥有经典的Ubuntu系统,并且所有可以安装在Ubuntu上的东西都可以在Dockerfile中编写。如果是CoreOS,那么您就没有那么多的自由,并且在启动映像的构建阶段就无法轻松添加自定义内核模块和软件包。