本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
使用 LTSP 為 Kubernetes 構建網路可引導的伺服器農場
在這篇文章中,我將向您介紹一項適用於 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-server
、filename
選項。 - 接下來,節點將應用設定並下載引導載入程式(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-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 playbook 為每個節點靜態配置租約。
嘗試執行您的第一個節點。如果一切正常,您將在那裡擁有一個正在執行的系統。該節點也將新增到您的 Kubernetes 叢集中。
現在您可以嘗試進行自己的更改。
如果您需要更多功能,請注意 LTSP 可以輕鬆更改以滿足您的需求。請隨時檢視原始碼,您可以在其中找到許多答案。
更新:許多人問我:為什麼不直接使用 CoreOS 和 Ignition?
我可以回答。這裡的主要特點是映象準備過程,而不是配置。在 LTSP 的情況下,您擁有經典的 Ubuntu 系統,任何可以在 Ubuntu 上安裝的東西都可以在 Dockerfile 中寫入。在 CoreOS 的情況下,您沒有那麼多自由,並且不能在引導映象的構建階段輕鬆新增自定義核心模組和軟體包。