本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

使用 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 的情況下,您沒有那麼多自由,並且不能在引導映象的構建階段輕鬆新增自定義核心模組和軟體包。