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

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

k8s+ltsp

在这篇文章中,我将向您介绍一项用于 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-serverfilename 选项。
  • 接下来,节点将应用设置并下载引导加载程序 (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-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 剧本来静态地为每个节点配置租约。

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

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

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

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

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