Содержание
k8s-release — инструмент сборки
k8s-release — инструмент для сборки воспроизводимых DEB/RPM-пакетов компонентов Kubernetes (kubelet, kube-apiserver, etcd, Flannel, Istio и др.) в герметичном Docker-окружении. Создаёт подписанные apt/yum-репозитории, SBOM, хеш-суммы и airgap-бандлы для закрытых, регулируемых и изолированных сред. В репозитории также лежит Terraform-модуль для развёртывания HA Kubernetes на микро-ВМ Firecracker: 3 control-plane, 1+ workers, Flannel, Istio, SELinux enforcing.
k8s-release собирает любые комбинации компонентов через флаг --component. Можно указать отдельные компоненты (kubelet, etcd, flannel, istio), all для всего сразу, и формат пакетов — deb, rpm или оба.
./k8s-release build v1.36.1 --component kubelet,kubectl --format deb,rpm ./k8s-release build v1.36.1 --component all --format deb ./k8s-release build 3.5.22 --component etcd --format rpm ./k8s-release build 1.30.0 --component istio --format deb,rpm ./k8s-release bundle v1.36.1 --airgap ./k8s-release verify-release v1.36.1 --repo ingresslabs/k8s-release ./k8s-release passport v1.36.1
Связь проектов
Оба проекта — части одной экосистемы. Первый проект, k8s-release, использует ту же платформу для сборки и развёртывания Kubernetes на микро-ВМ Firecracker. Terraform-модуль в k8s-release автоматизирует развёртывание HA-кластера по тем же принципам, что описаны в статье. Вторая статья описывает самодельную Firecracker CI-платформу (загрузка за 10 мс, кеширование, снапшоты).
Как использовать
Подготовка:
git clone https://github.com/ingresslabs/k8s-release.git cd k8s-release/terraform/k2vm-ha-lab cp terraform.tfvars.example terraform.tfvars
Настройка хоста (один из двух):
echo 'K8S_RELEASE_PROOF_HOST=root@192.168.1.100' > ../../.env
Или target_host = "root@192.168.1.100" в terraform.tfvars.
На удалённом хосте должны быть: Firecracker, Docker, ядро, initrd, модули и подготовленный rootfs.ext4.
Запуск:
terraform init && terraform apply terraform destroy
terraform { required_version = ">= 1.5.0" required_providers { local = { source = "hashicorp/local", version = "~> 2.5" } null = { source = "hashicorp/null", version = "~> 3.2" } } } variable "name" { type = string; default = "k8s-ha-lab" } variable "target_host" { type = string; default = "" } variable "target_user" { type = string; default = "root" } variable "github_repo" { type = string; default = "ingresslabs/k8s-release" } variable "github_run_id" { type = number; default = 26680027183 } variable "control_plane_count" { type = number; default = 3 } variable "worker_count" { type = number; default = 2 } variable "kubernetes_version" { type = string; default = "v1.36.1" } variable "subnet_prefix" { type = string; default = "198.19.2" } variable "network_plugin" { type = string; default = "flannel" } variable "firecracker_binary" { type = string; default = "/usr/local/bin/firecracker" } variable "kernel_path" { type = string; default = "/opt/fc-lab/vmlinux" } variable "initrd_path" { type = string; default = "/opt/fc-lab/initrd.img" } variable "kernel_modules_tar_path" { type = string; default = "/opt/fc-lab/modules.tar.gz" } variable "base_rootfs_path" { type = string; default = "/opt/fc-lab/rootfs.ext4" } variable "vcpu_count" { type = number; default = 2 } variable "guest_selinux_mode" { type = string; default = "permissive" } variable "enable_istio" { type = bool; default = false } variable "redeploy_token" { type = string; default = "" } locals { repo_root = abspath("${path.module}") env_path = "${local.repo_root}/.env" env_content = fileexists(local.env_path) ? file(local.env_path) : "" env_lines = [for l in split("\n", local.env_content) : trimspace(l) if l != "" && !startswith(l, "#")] env_pairs = [for l in local.env_lines : regex("^([A-Za-z_][A-Za-z0-9_]*)=(.*)$", l)] env_map = { for p in local.env_pairs : p[0] => trimsuffix(trimprefix(p[1], "\""), "\"") } target_source = var.target_host != "" ? var.target_host : lookup(local.env_map, "K8S_RELEASE_PROOF_HOST", "") parts = can(regex("^([^@]+)@(.+)$", local.target_source)) ? regex("^([^@]+)@(.+)$", local.target_source) : [] effective_user = length(local.parts) == 2 ? local.parts[0] : var.target_user effective_host = length(local.parts) == 2 ? local.parts[1] : local.target_source selinux_params = var.guest_selinux_mode == "disabled" ? ["apparmor=0", "selinux=0", "audit=0"] : var.guest_selinux_mode == "permissive" ? ["apparmor=0", "security=selinux", "selinux=1", "enforcing=0"] : ["apparmor=0", "security=selinux", "selinux=1", "enforcing=1"] kernel_boot = join(" ", concat( ["console=ttyS0", "reboot=k", "panic=1", "pci=off", "root=/dev/vda", "rw"], local.selinux_params)) manifest = { schema_version = "k2vm.spec.v1" name = var.name target = { host = local.effective_host, user = local.effective_user, workdir = "/root/${var.name}" } cluster = { distribution = "kubeadm" subnet_prefix = var.subnet_prefix control_plane_count = var.control_plane_count worker_count = var.worker_count network_plugin = var.network_plugin kubernetes_version = var.kubernetes_version control_plane_runtime = "static-pods" } firecracker = { binary = var.firecracker_binary bridge_name = "k2vmbr0" tap_prefix = "k2vmtap" kernel_source = "provided" kernel_path = var.kernel_path initrd_path = var.initrd_path kernel_modules_tar_path = var.kernel_modules_tar_path base_rootfs_path = var.base_rootfs_path kernel_params = local.kernel_boot vcpu_count = var.vcpu_count } guest = { selinux_mode = var.guest_selinux_mode } paths = { run_root = "/var/lib/k2vm-lab", cache_root = "/var/cache/k2vm-lab" } release = { enabled = true github_repo = var.github_repo github_run = { repo = var.github_repo, run_id = var.github_run_id } package_repository = { source = "github_run_artifact" artifact_layout = "component_packages" artifact_components = ["kubelet","kubectl", "kube-apiserver","kube-controller-manager", "kube-scheduler","kube-proxy","etcd","flannel"] mode = "hybrid" } } addons = { istio = { enabled = var.enable_istio, profile = "minimal" } } logging = { level = "INFO", format = "text" } } } resource "local_sensitive_file" "manifest" { filename = "${path.module}/${var.name}.rendered.json" content = jsonencode(local.manifest) } resource "null_resource" "deploy" { triggers = { sha = sha256(local_sensitive_file.manifest.content) token = var.redeploy_token } provisioner "local-exec" { interpreter = ["/bin/bash", "-lc"] command = <<-EOT set -e MANIFEST="${local_sensitive_file.manifest.filename}" HOST="${local.effective_user}@${local.effective_host}" scp "$MANIFEST" "$HOST:/tmp/lab-manifest.json" ssh $HOST "bash /opt/k2vm/k2vm-kubeadm-engine.sh apply /tmp/lab-manifest.json" EOT } provisioner "local-exec" { when = destroy interpreter = ["/bin/bash", "-lc"] command = <<-EOT ssh "${local.effective_user}@${local.effective_host}" \ "bash /opt/k2vm/k2vm-kubeadm-engine.sh delete || true" EOT } } output "manifest_path" { value = local_sensitive_file.manifest.filename } output "target_host" { value = local.effective_host }
HA-кластер на Firecracker
Это руководство выполняется непосредственно на хосте Linux. Оно собирает одну переиспользуемую гостевую rootfs, клонирует её на несколько дисков микро-ВМ Firecracker, загружает:
3 узла control-plane kubeadm
встроенный etcd, по одному участнику на узел control-plane
2 рабочих узла по умолчанию
Kubernetes API через хост-прокси HAProxy
Предварительные требования
Выполняйте всё от root на хосте Linux, где будет запущен Firecracker. На хосте необходимы:
Docker
Firecracker
curl, ip, iptables, ssh, scp, ssh-keygen
unsquashfs из squashfs-tools
mkfs.ext4, e2fsck, resize2fs из e2fsprogs
mount, umount, truncate, chroot
исходящий доступ в интернет для: загрузки ядра LinuxKit, Ubuntu rootfs, пакетов Kubernetes, Docker containerd, манифеста Flannel, образа HAProxy
Выберите подсеть, не пересекающуюся с другими мостами на хосте.
Экспорт переменных
Создаёт 3 control-plane и 2 worker-узла по умолчанию. Измените WORKER_COUNT при необходимости.
export RUN_ROOT=/var/lib/firecracker-kubeadm-ha export CACHE_ROOT=/var/cache/firecracker-kubeadm-ha export CONTROL_PLANE_COUNT=3 export WORKER_COUNT=2 export NODE_COUNT="$((CONTROL_PLANE_COUNT + WORKER_COUNT))" export SUBNET_PREFIX=198.19.0 export GATEWAY="${SUBNET_PREFIX}.1" export CIDR="${SUBNET_PREFIX}.0/24" export API_LB_IP="${SUBNET_PREFIX}.5" export API_LB_PORT=6443 export PRIMARY_CONTROL_PLANE_IP="${SUBNET_PREFIX}.10" export BRIDGE_NAME=k8sha198 export TAP_PREFIX=k8sha198 export FIRECRACKER_BIN=/usr/local/bin/firecracker export FIRECRACKER_ARCH="$(uname -m)" export LINUXKIT_KERNEL_IMAGE=linuxkit/kernel:6.12.59 export ROOTFS_SIZE_GIB=12 export CONTROL_PLANE_MEM_MIB=2048 export WORKER_MEM_MIB=1536 export VCPU_COUNT=2 export KUBERNETES_MINOR=v1.36 export KUBERNETES_VERSION=v1.36.1 export POD_CIDR=10.244.0.0/16 export SERVICE_CIDR=10.96.0.0/12 export CNI_PLUGINS_VERSION=v1.3.0 export NETWORK_PLUGIN=flannel export FLANNEL_MANIFEST_URL="https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml" export HAPROXY_IMAGE=haproxy:3.2.19-alpine export API_LB_CONTAINER_NAME="kubeadm-ha-api-lb-${BRIDGE_NAME}" export GUEST_SSH_KEY="${CACHE_ROOT}/lab_ssh_key" export GUEST_SSH_PUB="${GUEST_SSH_KEY}.pub" export ARTIFACT_PREFIX=/var/log/kubeadm-ha-lab export KERNEL_BOOT_ARGS="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw random.trust_cpu=on systemd.mask=serial-getty@ttyS0.service systemd.mask=systemd-random-seed.service"
Зависимости и SSH-ключ
Проверка зависимостей хоста и создание гостевого SSH-ключа.
command -v awk chroot curl docker e2fsck grep ip iptables mkfs.ext4 mount \ resize2fs sha256sum sort ssh scp ssh-keygen truncate umount unsquashfs >/dev/null test -x "${FIRECRACKER_BIN}" mkdir -p "${RUN_ROOT}" "${CACHE_ROOT}" if [[ ! -f "${GUEST_SSH_KEY}" || ! -f "${GUEST_SSH_PUB}" ]]; then ssh-keygen -q -t ed25519 -N "" -f "${GUEST_SSH_KEY}" fi
Вспомогательные функции
node_role() { if (( $1 < CONTROL_PLANE_COUNT )); then echo control-plane else echo worker fi } node_ip() { printf '%s.%d\n' "${SUBNET_PREFIX}" "$((10 + $1))" } node_name() { printf 'k8s-%02d\n' "$1" } node_tap() { printf '%s%d\n' "${TAP_PREFIX}" "$1" } node_mac() { printf '06:36:00:00:00:%02x\n' "$((16 + $1))" } node_mem() { if [[ "$(node_role "$1")" == "control-plane" ]]; then echo "${CONTROL_PLANE_MEM_MIB}" else echo "${WORKER_MEM_MIB}" fi } SSH_OPTS=(-i "${GUEST_SSH_KEY}" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5) node_ssh() { local idx="$1" shift ssh "${SSH_OPTS[@]}" "root@$(node_ip "${idx}")" "$@" } server_ssh() { node_ssh 0 "$@" } copy_guest_file() { local remote_path="$1" local local_path="$2" if server_ssh "test -f $(printf '%q' "${remote_path}")" >/dev/null 2>&1; then server_ssh "cat $(printf '%q' "${remote_path}")" > "${local_path}" fi } wait_for_ssh() { local ip="$1" for _ in $(seq 1 120); do if ssh "${SSH_OPTS[@]}" "root@${ip}" true >/dev/null 2>&1; then return 0 fi sleep 2 done return 1 } wait_for_node_ready() { local name="$1" for _ in $(seq 1 240); do if server_ssh "kubectl --kubeconfig /etc/kubernetes/admin.conf get node ${name} --no-headers 2>/dev/null | awk '\$2==\"Ready\"{ok=1} END{exit(ok?0:1)}'"; then return 0 fi sleep 2 done return 1 }
Скачивание ядра и rootfs
Используется образ ядра LinuxKit и последняя Ubuntu squashfs из Firecracker CI.
export LINUXKIT_KEY="$(printf '%s\n' "${LINUXKIT_KERNEL_IMAGE}" | sha256sum | awk '{print substr($1,1,16)}')" export LINUXKIT_CACHE_DIR="${CACHE_ROOT}/downloads/linuxkit-${LINUXKIT_KEY}" export KERNEL_PATH="${LINUXKIT_CACHE_DIR}/vmlinux" export KERNEL_BZIMAGE_PATH="${LINUXKIT_CACHE_DIR}/kernel" export KERNEL_MODULES_TAR_PATH="${LINUXKIT_CACHE_DIR}/kernel.tar" export KERNEL_DEV_TAR_PATH="${LINUXKIT_CACHE_DIR}/kernel-dev.tar" mkdir -p "${LINUXKIT_CACHE_DIR}" if [[ ! -f "${KERNEL_PATH}" || ! -f "${KERNEL_MODULES_TAR_PATH}" ]]; then rm -rf "${LINUXKIT_CACHE_DIR}.tmp" mkdir -p "${LINUXKIT_CACHE_DIR}.tmp" docker pull "${LINUXKIT_KERNEL_IMAGE}" >/dev/null cid="$(docker create "${LINUXKIT_KERNEL_IMAGE}" /bin/sh)" docker cp "${cid}:/kernel" "${LINUXKIT_CACHE_DIR}.tmp/kernel" docker cp "${cid}:/kernel.tar" "${LINUXKIT_CACHE_DIR}.tmp/kernel.tar" docker cp "${cid}:/kernel-dev.tar" "${LINUXKIT_CACHE_DIR}.tmp/kernel-dev.tar" docker rm -f "${cid}" >/dev/null export LINUXKIT_HEADERS_DIR="$(tar -tf "${LINUXKIT_CACHE_DIR}.tmp/kernel-dev.tar" \ | sed -n 's#^\(usr/src/linux-headers-[^/]*\)/scripts/extract-vmlinux$#\1#p' \ | head -n 1)" tar -xOf "${LINUXKIT_CACHE_DIR}.tmp/kernel-dev.tar" \ "${LINUXKIT_HEADERS_DIR}/scripts/extract-vmlinux" \ > "${LINUXKIT_CACHE_DIR}.tmp/extract-vmlinux" chmod +x "${LINUXKIT_CACHE_DIR}.tmp/extract-vmlinux" "${LINUXKIT_CACHE_DIR}.tmp/extract-vmlinux" \ "${LINUXKIT_CACHE_DIR}.tmp/kernel" \ > "${LINUXKIT_CACHE_DIR}.tmp/vmlinux" mv "${LINUXKIT_CACHE_DIR}.tmp/kernel" "${KERNEL_BZIMAGE_PATH}" mv "${LINUXKIT_CACHE_DIR}.tmp/vmlinux" "${KERNEL_PATH}" mv "${LINUXKIT_CACHE_DIR}.tmp/kernel.tar" "${KERNEL_MODULES_TAR_PATH}" mv "${LINUXKIT_CACHE_DIR}.tmp/kernel-dev.tar" "${KERNEL_DEV_TAR_PATH}" rm -rf "${LINUXKIT_CACHE_DIR}.tmp" fi
export FIRECRACKER_CI_VERSION="$("${FIRECRACKER_BIN}" --version | awk '{print $2}' | sed 's/\.[0-9]*$//')" export UBUNTU_KEY="$(curl -fsSL "https://s3.amazonaws.com/spec.ccfc.min?prefix=firecracker-ci/${FIRECRACKER_CI_VERSION}/${FIRECRACKER_ARCH}/ubuntu-&list-type=2" \ | grep -oP "(?<=<Key>)(firecracker-ci/${FIRECRACKER_CI_VERSION}/${FIRECRACKER_ARCH}/ubuntu-[0-9]+\.[0-9]+\.squashfs)(?=</Key>)" \ | sort -V | tail -1)" export ROOTFS_SQUASHFS_PATH="${CACHE_ROOT}/downloads/$(basename "${UBUNTU_KEY}")" mkdir -p "${CACHE_ROOT}/downloads" if [[ ! -f "${ROOTFS_SQUASHFS_PATH}" ]]; then curl -fsSL "https://s3.amazonaws.com/spec.ccfc.min/${UBUNTU\\_KEY}" -o "${ROOTFS_SQUASHFS_PATH}" fi
Базовая ext4 rootfs
Преобразует squashfs в образ ext4 и внедряет гостевой SSH-ключ.
export BASE_KEY="$({ sha256sum "${ROOTFS_SQUASHFS_PATH}"; sha256sum "${GUEST_SSH_PUB}"; } | sha256sum | awk '{print substr($1,1,16)}')" export BASE_ROOTFS_PATH="${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.ext4" mkdir -p "${CACHE_ROOT}/base" if [[ ! -s "${BASE_ROOTFS_PATH}" ]]; then rm -rf "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs" rm -f "${BASE_ROOTFS_PATH}.tmp" mkdir -p "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs" unsquashfs -d "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs/rootfs" \ "${ROOTFS_SQUASHFS_PATH}" >/dev/null mkdir -p "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs/rootfs/root/.ssh" chmod 700 "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs/rootfs/root/.ssh" cp "${GUEST_SSH_PUB}" \ "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs/rootfs/root/.ssh/authorized_keys" chmod 600 "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs/rootfs/root/.ssh/authorized_keys" truncate -s 4G "${BASE_ROOTFS_PATH}.tmp" mkfs.ext4 -d "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs/rootfs" \ -F "${BASE_ROOTFS_PATH}.tmp" >/dev/null mv "${BASE_ROOTFS_PATH}.tmp" "${BASE_ROOTFS_PATH}" rm -rf "${CACHE_ROOT}/base/ubuntu-${BASE_KEY}.rootfs" fi
Гостевой образ с kubeadm
Устанавливает: модули ядра, containerd, kubelet, kubeadm, kubectl, плагины CNI, системные параметры Kubernetes и базовую настройку служб.
export PREPARED_KEY="$({ sha256sum "${BASE_ROOTFS_PATH}" "${KERNEL_PATH}" "${GUEST_SSH_PUB}" "${KERNEL_MODULES_TAR_PATH}"; printf 'kubernetes_minor=%s\n' "${KUBERNETES_MINOR}"; printf 'kubernetes_version=%s\n' "${KUBERNETES_VERSION}"; printf 'cni_plugins=%s\n' "${CNI_PLUGINS_VERSION}"; printf 'rootfs_size_gib=%s\n' "${ROOTFS_SIZE_GIB}"; printf 'generation=manual-kubeadm-firecracker-ha-v1\n'; } | sha256sum | awk '{print substr($1,1,16)}')" export PREPARED_ROOTFS_PATH="${CACHE_ROOT}/prepared-${PREPARED_KEY}.ext4" if [[ ! -s "${PREPARED_ROOTFS_PATH}" ]]; then cp --reflink=auto "${BASE_ROOTFS_PATH}" "${PREPARED_ROOTFS_PATH}.tmp" \ 2>/dev/null || cp "${BASE_ROOTFS_PATH}" "${PREPARED_ROOTFS_PATH}.tmp" e2fsck -fy "${PREPARED_ROOTFS_PATH}.tmp" >/dev/null 2>&1 || true truncate -s "${ROOTFS_SIZE_GIB}G" "${PREPARED_ROOTFS_PATH}.tmp" resize2fs "${PREPARED_ROOTFS_PATH}.tmp" >/dev/null export MNT="${CACHE_ROOT}/mnt-${PREPARED_KEY}" mkdir -p "${MNT}" mount -o loop "${PREPARED_ROOTFS_PATH}.tmp" "${MNT}" mount -t proc proc "${MNT}/proc" mount -t sysfs sysfs "${MNT}/sys" mount --bind /dev "${MNT}/dev" mount -t devpts devpts "${MNT}/dev/pts" mount --bind /run "${MNT}/run" rm -f "${MNT}/etc/resolv.conf" printf 'nameserver 1.1.1.1\nnameserver 8.8.8.8\n' > "${MNT}/etc/resolv.conf" rm -rf "${MNT}/lib/modules" mkdir -p "${MNT}/lib" tar -xf "${KERNEL_MODULES_TAR_PATH}" -C "${MNT}" ./lib/modules chmod 1777 "${MNT}/tmp" mkdir -p "${MNT}/dev/pts" "${MNT}/var/cache/apt/archives/partial" \ "${MNT}/var/lib/apt/lists/partial" "${MNT}/var/log/apt" touch "${MNT}/var/log/dpkg.log" mkdir -p "${MNT}/root/.ssh" && chmod 700 "${MNT}/root/.ssh" cp "${GUEST_SSH_PUB}" "${MNT}/root/.ssh/authorized_keys" chmod 600 "${MNT}/root/.ssh/authorized_keys" chroot "${MNT}" apt-get update DEBIAN_FRONTEND=noninteractive chroot "${MNT}" apt-get install -y \ --no-install-recommends apt-transport-https ca-certificates conntrack \ curl ebtables ethtool gpg iptables ipset jq openssh-server socat tar xz-utils mkdir -p "${MNT}/etc/apt/keyrings" "${MNT}/etc/apt/sources.list.d" docker_arch="$(chroot "${MNT}" dpkg --print-architecture)" docker_codename="$(awk -F= '/^VERSION_CODENAME=/{print $2}' "${MNT}/etc/os-release")" chroot "${MNT}" curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" \ | chroot "${MNT}" gpg --dearmor -o /etc/apt/keyrings/docker.gpg printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu %s stable\n' \ "${docker_arch}" "${docker_codename}" > "${MNT}/etc/apt/sources.list.d/docker.list" chroot "${MNT}" curl -fsSL "https://pkgs.k8s.io/core:/stable:/${KUBERNETES_MINOR}/deb/Release.key" \ | chroot "${MNT}" gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg printf 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/%s/deb/ /\n' \ "${KUBERNETES_MINOR}" > "${MNT}/etc/apt/sources.list.d/kubernetes.list" chroot "${MNT}" apt-get update DEBIAN_FRONTEND=noninteractive chroot "${MNT}" apt-get install -y \ containerd.io "kubelet=${KUBERNETES_VERSION#v}-*" \ "kubeadm=${KUBERNETES_VERSION#v}-*" "kubectl=${KUBERNETES_VERSION#v}-*" chroot "${MNT}" apt-mark hold kubelet kubeadm kubectl >/dev/null 2>&1 || true mkdir -p "${MNT}/opt/cni/bin" tar -C "${MNT}/opt/cni/bin" -xzf "${CNI_ARCHIVE}" mkdir -p "${MNT}/etc/containerd" chroot "${MNT}" containerd config default > "${MNT}/etc/containerd/config.toml" sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' "${MNT}/etc/containerd/config.toml" cat > "${MNT}/etc/modules-load.d/kubernetes.conf" <<'EOF' overlay br_netfilter EOF cat > "${MNT}/etc/sysctl.d/99-kubernetes.conf" <<'EOF' net.bridge.bridge-nf-call-iptables=1 net.bridge.bridge-nf-call-ip6tables=1 net.ipv4.ip_forward=1 EOF chroot "${MNT}" update-alternatives --set iptables /usr/sbin/iptables-legacy chroot "${MNT}" update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy umount "${MNT}/dev/pts" || true umount "${MNT}/proc" || true umount "${MNT}/sys" || true umount "${MNT}/dev" || true umount "${MNT}/run" || true umount "${MNT}" || true mv "${PREPARED_ROOTFS_PATH}.tmp" "${PREPARED_ROOTFS_PATH}" fi
Мост, NAT и HAProxy
Явные правила FORWARD важны на хостах, где политика FORWARD по умолчанию — DROP.
docker rm -f "${API_LB_CONTAINER_NAME}" >/dev/null 2>&1 || true ip link add name "${BRIDGE_NAME}" type bridge 2>/dev/null || true ip addr add "${GATEWAY}/24" dev "${BRIDGE_NAME}" 2>/dev/null || true ip addr add "${API_LB_IP}/24" dev "${BRIDGE_NAME}" 2>/dev/null || true ip link set "${BRIDGE_NAME}" up sysctl -w net.ipv4.ip_forward=1 >/dev/null iptables -C FORWARD -i "${BRIDGE_NAME}" -j ACCEPT 2>/dev/null \ || iptables -A FORWARD -i "${BRIDGE_NAME}" -j ACCEPT iptables -C FORWARD -o "${BRIDGE_NAME}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null \ || iptables -A FORWARD -o "${BRIDGE_NAME}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT iptables -t nat -C POSTROUTING -s "${CIDR}" ! -o "${BRIDGE_NAME}" -j MASQUERADE 2>/dev/null \ || iptables -t nat -A POSTROUTING -s "${CIDR}" ! -o "${BRIDGE_NAME}" -j MASQUERADE cat > "${RUN_ROOT}/haproxy.cfg" <<EOF global maxconn 2048 defaults mode tcp timeout connect 5s timeout client 60s timeout server 60s frontend kube_api bind ${API_LB_IP}:${API_LB_PORT} default_backend kube_apis backend kube_apis balance roundrobin option tcp-check default-server inter 2s fall 3 rise 2 EOF for i in $(seq 0 "$((CONTROL_PLANE_COUNT - 1))"); do printf ' server cp%s %s:6443 check\n' "${i}" "$(node_ip "${i}")" \ >> "${RUN_ROOT}/haproxy.cfg" done docker run -d --name "${API_LB_CONTAINER_NAME}" --network host \ -v "${RUN_ROOT}/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro" \ "${HAPROXY_IMAGE}" >/dev/null
Создание микро-ВМ
Каждая ВМ получает клонированный диск ext4, имя хоста, /etc/hosts и конфигурацию systemd-networkd.
for i in $(seq 0 "$((NODE_COUNT - 1))"); do vm_dir="${RUN_ROOT}/nodes/node${i}" mkdir -p "${vm_dir}" cp --reflink=auto "${PREPARED_ROOTFS_PATH}" "${vm_dir}/rootfs.ext4" \ 2>/dev/null || cp "${PREPARED_ROOTFS_PATH}" "${vm_dir}/rootfs.ext4" e2fsck -fy "${vm_dir}/rootfs.ext4" >/dev/null 2>&1 || true mkdir -p "${vm_dir}/mnt" mount -o loop "${vm_dir}/rootfs.ext4" "${vm_dir}/mnt" printf '%s\n' "$(node_name "${i}")" > "${vm_dir}/mnt/etc/hostname" { printf '127.0.0.1 localhost\n' printf '127.0.1.1 %s\n' "$(node_name "${i}")" printf '%s api-lb\n' "${API_LB_IP}" for j in $(seq 0 "$((NODE_COUNT - 1))"); do printf '%s %s\n' "$(node_ip "${j}")" "$(node_name "${j}")" done } > "${vm_dir}/mnt/etc/hosts" mkdir -p "${vm_dir}/mnt/etc/systemd/network" cat > "${vm_dir}/mnt/etc/systemd/network/20-eth0.network" <<EOF [Match] Name=eth0 [Network] Address=$(node_ip "${i}")/24 Gateway=${GATEWAY} DNS=1.1.1.1 DNS=8.8.8.8 EOF rm -f "${vm_dir}/mnt/etc/machine-id" touch "${vm_dir}/mnt/etc/machine-id" rm -rf "${vm_dir}/mnt/etc/kubernetes" "${vm_dir}/mnt/var/lib/etcd" mkdir -p "${vm_dir}/mnt/var/lib/containerd" "${vm_dir}/mnt/etc/cni/net.d" umount "${vm_dir}/mnt" tap="$(node_tap "${i}")" mac="$(node_mac "${i}")" mem="$(node_mem "${i}")" ip tuntap add dev "${tap}" mode tap 2>/dev/null || true ip link set "${tap}" master "${BRIDGE_NAME}" ip link set "${tap}" up cat > "${vm_dir}/vm.json" <<EOF {"boot-source":{"kernel_image_path":"${KERNEL_PATH}","boot_args":"${KERNEL_BOOT_ARGS}"},"drives":[{"drive_id":"rootfs","path_on_host":"${vm_dir}/rootfs.ext4","is_root_device":true,"is_read_only":false}],"machine-config":{"vcpu_count":${VCPU_COUNT},"mem_size_mib":${mem}},"network-interfaces":[{"iface_id":"eth0","host_dev_name":"${tap}","guest_mac":"${mac}"}],"logger":{"log_path":"${vm_dir}/firecracker.log","level":"Info","show_level":true,"show_log_origin":true}} EOF "${FIRECRACKER_BIN}" --api-sock "${vm_dir}/fc.sock" \ --config-file "${vm_dir}/vm.json" > "${vm_dir}/console.log" 2>&1 & echo $! > "${vm_dir}/pid" done
Подготовка узлов
Ожидание SSH и подготовка узлов.
for i in $(seq 0 "$((NODE_COUNT - 1))"); do wait_for_ssh "$(node_ip "${i}")" done for i in $(seq 0 "$((NODE_COUNT - 1))"); do node_ssh "${i}" "cat >/root/prepare-node.sh" <<'EOF' #!/usr/bin/env bash set -euo pipefail swapoff -a || true sed -i '/ swap / s/^/#/' /etc/fstab 2>/dev/null || true modprobe overlay || true modprobe br_netfilter || true sysctl --system >/dev/null systemctl daemon-reload || true systemctl enable --now ssh containerd kubelet >/dev/null 2>&1 || true systemctl restart containerd kubeadm reset -f >/dev/null 2>&1 || true rm -rf /etc/kubernetes /var/lib/etcd /var/lib/cni /var/lib/kubelet/* /etc/cni/net.d/* mkdir -p /etc/cni/net.d kubeadm config images pull --kubernetes-version="$(kubeadm version -o short)" >/dev/null 2>&1 || true EOF node_ssh "${i}" "chmod +x /root/prepare-node.sh && /root/prepare-node.sh" done
Инициализация control-plane
server_ssh "cat >/root/init-primary.sh" <<EOF #!/usr/bin/env bash set -euo pipefail version="${KUBERNETES_VERSION}" kubeadm init \ --kubernetes-version "\${version}" \ --control-plane-endpoint "${API_LB_IP}:${API_LB_PORT}" \ --apiserver-advertise-address "${PRIMARY_CONTROL_PLANE_IP}" \ --pod-network-cidr "${POD_CIDR}" \ --service-cidr "${SERVICE_CIDR}" \ --upload-certs \ --ignore-preflight-errors=all mkdir -p /root/.kube cp /etc/kubernetes/admin.conf /root/.kube/config certificate_key="\$(kubeadm init phase upload-certs --upload-certs 2>/dev/null | tail -n 1)" join_cmd="\$(kubeadm token create --print-join-command)" printf '%s\n' "\${certificate_key}" >/root/certificate-key.txt printf '%s\n' "\${join_cmd}" >/root/join-command.txt EOF server_ssh "chmod +x /root/init-primary.sh && /root/init-primary.sh"
Установка Flannel
server_ssh "kubectl --kubeconfig /etc/kubernetes/admin.conf apply -f $(printf '%q' "${FLANNEL_MANIFEST_URL}")" for _ in $(seq 1 180); do flannel_ns="$(server_ssh "kubectl --kubeconfig /etc/kubernetes/admin.conf get daemonset -A --no-headers 2>/dev/null | awk '\$2==\"kube-flannel-ds\"{print \$1; exit}'" || true)" if [[ -n "${flannel_ns}" ]]; then if server_ssh "kubectl --kubeconfig /etc/kubernetes/admin.conf -n ${flannel_ns} rollout status daemonset/kube-flannel-ds --timeout=5s" >/dev/null 2>&1; then break fi fi sleep 2 done
Подключение узлов
join_cmd="$(server_ssh "cat /root/join-command.txt")" certificate_key="$(server_ssh "cat /root/certificate-key.txt")" # Control-plane узлы for i in 1 2; do ip="$(node_ip "${i}")" node_ssh "${i}" "cat >/root/join-control-plane.sh" <<EOF #!/usr/bin/env bash set -euo pipefail ${join_cmd} --control-plane --certificate-key ${certificate_key} --apiserver-advertise-address ${ip} --ignore-preflight-errors=all EOF node_ssh "${i}" "chmod +x /root/join-control-plane.sh && /root/join-control-plane.sh" wait_for_node_ready "$(node_name "${i}")" done # Worker-узлы for i in $(seq "${CONTROL_PLANE_COUNT}" "$((NODE_COUNT - 1))"); do node_ssh "${i}" "cat >/root/join-worker.sh" <<EOF #!/usr/bin/env bash set -euo pipefail ${join_cmd} --ignore-preflight-errors=all EOF node_ssh "${i}" "chmod +x /root/join-worker.sh && /root/join-worker.sh" wait_for_node_ready "$(node_name "${i}")" done
Готовность кластера
Ожидаемый результат:
k8s-00 Ready control-plane k8s-01 Ready control-plane k8s-02 Ready control-plane k8s-03 Ready <none> k8s-04 Ready <none>
Проверка etcd
Должно отобразиться три участника etcd.
etcd_pod="etcd-$(node_name 0)" server_ssh "kubectl --kubeconfig /etc/kubernetes/admin.conf \ -n kube-system exec ${etcd_pod} -- etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \ --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \ member list -w table"
Smoke-тест
server_ssh "cat >/root/run-smoke.sh" <<'EOSMOKE' #!/usr/bin/env bash set -euo pipefail kubectl --kubeconfig /etc/kubernetes/admin.conf create namespace smoke \ --dry-run=client -o yaml | kubectl apply -f - kubectl apply -f - <<'YAML' apiVersion: apps/v1 kind: DaemonSet metadata: name: node-smoke namespace: smoke spec: selector: matchLabels: app: node-smoke template: metadata: labels: app: node-smoke spec: tolerations: - operator: Exists containers: - name: node-smoke image: busybox:1.36 command: ["sh", "-lc", "sleep 3600"] --- apiVersion: apps/v1 kind: Deployment metadata: name: echo namespace: smoke spec: replicas: 2 selector: matchLabels: app: echo template: metadata: labels: app: echo spec: tolerations: - operator: Exists containers: - name: echo image: nginx:1.27-alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: echo namespace: smoke spec: selector: app: echo ports: - port: 80 targetPort: 80 YAML kubectl -n smoke rollout status daemonset/node-smoke --timeout=240s kubectl -n smoke rollout status deployment/echo --timeout=240s kubectl -n kube-system rollout status deployment/coredns --timeout=240s kubectl apply -f - <<'YAML' apiVersion: batch/v1 kind: Job metadata: name: dns-http namespace: smoke spec: backoffLimit: 0 template: spec: restartPolicy: Never tolerations: - operator: Exists containers: - name: dns-http image: busybox:1.36 command: - sh - -lc - | for _ in $(seq 1 30); do nslookup echo.smoke.svc.cluster.local && \ wget -qO- http://echo.smoke.svc.cluster.local >/tmp/echo.html && \ test -s /tmp/echo.html && exit 0 sleep 2 done exit 1 YAML kubectl -n smoke wait --for=condition=complete job/dns-http --timeout=240s kubectl get nodes -o wide > /var/log/kubeadm-ha-lab.nodes 2>&1 kubectl get pods -A -o wide > /var/log/kubeadm-ha-lab.pods 2>&1 kubectl -n smoke get pods -o wide > /var/log/kubeadm-ha-lab.smoke-pods 2>&1 kubectl -n smoke logs job/dns-http > /var/log/kubeadm-ha-lab.smoke-job.log 2>&1 kubectl config view --raw > /var/log/kubeadm-ha-lab.kubeconfig 2>&1 EOSMOKE server_ssh "chmod +x /root/run-smoke.sh && /root/run-smoke.sh"
Сохранение артефактов
export KUBECONFIG="${RUN_ROOT}/artifacts/kubeconfig.yaml" kubectl get nodes -o wide kubectl get pods -A -o wide
Демонтаж
Остановка Firecracker, удаление tap, правил iptables, моста и RUN_ROOT. CACHE_ROOT сохраняется для переиспользования.
docker rm -f "${API_LB_CONTAINER_NAME}" >/dev/null 2>&1 || true for pid_file in "${RUN_ROOT}"/nodes/*/pid; do [[ -f "${pid_file}" ]] || continue kill "$(cat "${pid_file}")" 2>/dev/null || true done sleep 1 for pid_file in "${RUN_ROOT}"/nodes/*/pid; do [[ -f "${pid_file}" ]] || continue kill -9 "$(cat "${pid_file}")" 2>/dev/null || true done for i in $(seq 0 "$((NODE_COUNT - 1))"); do ip link del "$(node_tap "${i}")" 2>/dev/null || true done iptables -D FORWARD -i "${BRIDGE_NAME}" -j ACCEPT 2>/dev/null || true iptables -D FORWARD -o "${BRIDGE_NAME}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true iptables -t nat -D POSTROUTING -s "${CIDR}" ! -o "${BRIDGE_NAME}" -j MASQUERADE 2>/dev/null || true ip link set "${BRIDGE_NAME}" down 2>/dev/null || true ip link del "${BRIDGE_NAME}" type bridge 2>/dev/null || true rm -rf "${RUN_ROOT}"
Создай свой Talos или CoreOS Linux I
Антон Крылов | 6 мин чтения | 29 мая 2026 г.
В отличие от предыдущего руководства с полным HA-кластером (3 control-plane, kubeadm, etcd, HAProxy, ext4 rootfs на 12 ГБ), здесь подход намеренно минималистичен: k3s вместо kubeadm, initrd вместо тяжёлой корневой файловой системы, один control-plane вместо трёх. Никакого балансировщика, никакого внешнего etcd — только ядро, initrd и контейнеры. Это чертёж не прод-кластера, а идеи: Kubernetes-ОС может помещаться в оперативной памяти.
Talos показывает, что ОС, ориентированная на Kubernetes, может быть крошечной, неизменяемой и целенаправленной — так же, как CoreOS когда-то делала для контейнерных хостов. Но более интересная идея в том, что этот подход не является магией или прерогативой одного вендора. С помощью таких инструментов, как LinuxKit, минимального загрузочного образа и компактной среды выполнения Kubernetes, вы можете собрать собственную урезанную ОС для узлов: настраиваемую, повторяемую и содержащую только те компоненты, которые действительно нужны вашему кластеру.
Предварительные требования (Talos)
Выполняйте всё от root на хосте Linux, где будет запущен Firecracker. На хосте необходимы:
Docker
Firecracker
ip, iptables, ssh, ssh-keygen, openssl
исходящий доступ в интернет для загрузки контейнерных образов
Выберите подсеть, не пересекающуюся с другими мостами на хосте.
1. Экспорт переменных
export RUN_ROOT=/var/lib/create-your-own-talos export NODE_COUNT=3 export SUBNET_PREFIX=198.19.0 export SERVER_IP="${SUBNET_PREFIX}.10" export GATEWAY="${SUBNET_PREFIX}.1" export CIDR="${SUBNET_PREFIX}.0/24" export BRIDGE_NAME=lkt198 export TAP_PREFIX=lkt198 export FIRECRACKER_BIN=/usr/local/bin/firecracker export LINUXKIT_VERSION=v1.8.2 export LINUXKIT_BIN=/root/linuxkit/bin/linuxkit export LINUXKIT_KERNEL_IMAGE=linuxkit/kernel:6.12.59 export K3S_IMAGE=rancher/k3s:v1.36.1-k3s1 export K3S_TOKEN="$(openssl rand -hex 24)" export GUEST_SSH_KEY="${RUN_ROOT}/lab_ssh_key" export GUEST_SSH_PUB="${GUEST_SSH_KEY}.pub"
2. LinuxKit и SSH
command -v docker ip iptables ssh ssh-keygen openssl >/dev/null test -x "${FIRECRACKER_BIN}" mkdir -p "$(dirname "${LINUXKIT_BIN}")" "${RUN_ROOT}" if [[ ! -x "${LINUXKIT_BIN}" ]]; then curl -fsSL \ "https://github.com/linuxkit/linuxkit/releases/download/${LINUXKIT_VERSION}/linuxkit-linux-amd64" \ -o "${LINUXKIT_BIN}" chmod +x "${LINUXKIT_BIN}" fi if [[ ! -f "${GUEST_SSH_KEY}" || ! -f "${GUEST_SSH_PUB}" ]]; then ssh-keygen -q -t ed25519 -N "" -f "${GUEST_SSH_KEY}" fi
3. Вспомогательные функции
node_ip() { printf '%s.%d\n' "${SUBNET_PREFIX}" "$((10 + $1))" } node_name() { printf 'linuxkit-%02d\n' "$1" } node_tap() { printf '%s%d\n' "${TAP_PREFIX}" "$1" } node_mac() { printf '06:19:80:00:00:%02x\n' "$((16 + $1))" } node_mem() { if [[ "$1" == "0" ]]; then echo 2048 else echo 1536 fi } SSH_OPTS=(-n -i "${GUEST_SSH_KEY}" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR) server_ssh() { ssh "${SSH_OPTS[@]}" "root@${SERVER_IP}" "$@" } k3s_exec() { local cmd="$1" local exec_id="x$(date +%s%N)" server_ssh "ctr -n services.linuxkit tasks exec --exec-id ${exec_id} k3s /bin/sh -lc $(printf '%q' "${cmd}")" }
4. LinuxKit YAML
Control plane: k3s server с --cluster-init. Рабочие узлы: k3s agent.
kernel: image: ${LINUXKIT_KERNEL_IMAGE} cmdline: "console=tty0 console=ttyS0 console=ttyAMA0" init: - linuxkit/init:b5506cc74a6812dc40982cacfd2f4328f8a4b12a - linuxkit/runc:9442aa234715e751a16144f1d4ae3fd1a00fd492 - linuxkit/containerd:ba19f64efd3331a8fd0a33e00eabd14f6ee1780e - linuxkit/ca-certificates:256f1950df59f2f209e9f0b81374177409eb11de onboot: - name: sysctl image: linuxkit/sysctl:43ac1d39da329c3567fcb9689e5ca99de6d169b6 - name: ip image: linuxkit/ip:3c0676ee83a0dc739685be1253b8326f08581ef7 binds: - /etc/ip:/etc/ip command: ["ip", "-b", "/etc/ip/eth0.conf"] services: - name: getty image: linuxkit/getty:a86d74c8f89be8956330c3b115b0b1f2e09ef6e0 env: - INSECURE=true - name: rngd image: linuxkit/rngd:984eb580ecb63986f07f626b61692a97aacd7198 - name: sshd image: linuxkit/sshd:08e5d4a46603eff485d5d1b14001cc932a530858 binds.add: - /root/.ssh:/root/.ssh - name: k3s image: ${K3S_IMAGE} capabilities: - all net: host rootfsPropagation: shared devices: - path: /dev/kmsg type: c major: 1 minor: 11 mode: "0666" mounts: - type: cgroup options: ["rw","nosuid","noexec","nodev","relatime"] binds: - /etc/resolv.conf:/etc/resolv.conf - /lib/modules:/lib/modules - /var:/var:rshared,rbind env: - PATH=/bin:/bin/aux:/usr/bin:/sbin:/usr/sbin - K3S_TOKEN=${K3S_TOKEN} command: - /bin/k3s - server - --cluster-init - --node-name=linuxkit-00 - --node-ip=${SERVER_IP} - --advertise-address=${SERVER_IP} - --bind-address=0.0.0.0 - --tls-san=${SERVER_IP} - --write-kubeconfig-mode=0644 - --disable=traefik - --disable=servicelb - --disable=metrics-server - --disable=local-storage - --disable-cloud-controller - --disable-network-policy - --flannel-iface=eth0 - --flannel-backend=host-gw files: - path: etc/ip/eth0.conf contents: | address add ${SERVER_IP}/24 dev eth0 link set eth0 up route add default via ${GATEWAY} dev eth0 - path: etc/resolv.conf contents: | nameserver 1.1.1.1 nameserver 8.8.8.8 - path: root/.ssh/authorized_keys source: ${GUEST_SSH_PUB} mode: "0600"
kernel: image: ${LINUXKIT_KERNEL_IMAGE} cmdline: "console=tty0 console=ttyS0 console=ttyAMA0" init: - linuxkit/init:b5506cc74a6812dc40982cacfd2f4328f8a4b12a - linuxkit/runc:9442aa234715e751a16144f1d4ae3fd1a00fd492 - linuxkit/containerd:ba19f64efd3331a8fd0a33e00eabd14f6ee1780e - linuxkit/ca-certificates:256f1950df59f2f209e9f0b81374177409eb11de onboot: - name: sysctl image: linuxkit/sysctl:43ac1d39da329c3567fcb9689e5ca99de6d169b6 - name: ip image: linuxkit/ip:3c0676ee83a0dc739685be1253b8326f08581ef7 binds: - /etc/ip:/etc/ip command: ["ip", "-b", "/etc/ip/eth0.conf"] services: - name: getty image: linuxkit/getty:a86d74c8f89be8956330c3b115b0b1f2e09ef6e0 env: - INSECURE=true - name: rngd image: linuxkit/rngd:984eb580ecb63986f07f626b61692a97aacd7198 - name: sshd image: linuxkit/sshd:08e5d4a46603eff485d5d1b14001cc932a530858 binds.add: - /root/.ssh:/root/.ssh - name: k3s image: ${K3S_IMAGE} capabilities: - all net: host rootfsPropagation: shared devices: - path: /dev/kmsg type: c major: 1 minor: 11 mode: "0666" mounts: - type: cgroup options: ["rw","nosuid","noexec","nodev","relatime"] binds: - /etc/resolv.conf:/etc/resolv.conf - /lib/modules:/lib/modules - /var:/var:rshared,rbind env: - PATH=/bin:/bin/aux:/usr/bin:/sbin:/usr/sbin - K3S_TOKEN=${K3S_TOKEN} command: - /bin/k3s - agent - --server=https://${SERVER\\_IP}:6443 - --node-name=__NODE_NAME__ - --node-ip=__NODE_IP__ - --flannel-iface=eth0 files: - path: etc/ip/eth0.conf contents: | address add __NODE_IP__/24 dev eth0 link set eth0 up route add default via ${GATEWAY} dev eth0 - path: etc/resolv.conf contents: | nameserver 1.1.1.1 nameserver 8.8.8.8 - path: root/.ssh/authorized_keys source: ${GUEST_SSH_PUB} mode: "0600"
5. Сборка образов
mkdir -p "${RUN_ROOT}/nodes/node0" cp "${RUN_ROOT}/server.yml" "${RUN_ROOT}/nodes/node0/node.yml" "${LINUXKIT_BIN}" build \ --decompress-kernel \ --dir "${RUN_ROOT}/nodes/node0" \ --name node \ -f kernel+initrd \ "${RUN_ROOT}/nodes/node0/node.yml" for i in 1 2; do ip="$(node_ip "${i}")" name="$(node_name "${i}")" dir="${RUN_ROOT}/nodes/node${i}" mkdir -p "${dir}" sed \ -e "s/__NODE_NAME__/${name}/g" \ -e "s/__NODE_IP__/${ip}/g" \ "${RUN_ROOT}/agent.yml.tmpl" > "${dir}/node.yml" "${LINUXKIT_BIN}" build \ --decompress-kernel \ --dir "${dir}" \ --name node \ -f kernel+initrd \ "${dir}/node.yml" done
6. Мост и NAT
ip link add name "${BRIDGE_NAME}" type bridge 2>/dev/null || true ip addr add "${GATEWAY}/24" dev "${BRIDGE_NAME}" 2>/dev/null || true ip link set "${BRIDGE_NAME}" up sysctl -w net.ipv4.ip_forward=1 iptables -C FORWARD -i "${BRIDGE_NAME}" -j ACCEPT 2>/dev/null || iptables -A FORWARD -i "${BRIDGE_NAME}" -j ACCEPT iptables -C FORWARD -o "${BRIDGE_NAME}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ iptables -A FORWARD -o "${BRIDGE_NAME}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT iptables -t nat -C POSTROUTING -s "${CIDR}" ! -o "${BRIDGE_NAME}" -j MASQUERADE 2>/dev/null || \ iptables -t nat -A POSTROUTING -s "${CIDR}" ! -o "${BRIDGE_NAME}" -j MASQUERADE
7. Загрузка микро-ВМ
for i in 0 1 2; do dir="${RUN_ROOT}/nodes/node${i}" tap="$(node_tap "${i}")" mac="$(node_mac "${i}")" mem="$(node_mem "${i}")" ip tuntap add dev "${tap}" mode tap 2>/dev/null || true ip link set "${tap}" master "${BRIDGE_NAME}" ip link set "${tap}" up cat > "${dir}/vm.json" <<EOF {"boot-source":{"kernel_image_path":"${dir}/node-kernel","initrd_path":"${dir}/node-initrd.img","boot_args":"$(tr -d '\n' <"${dir}/node-cmdline") reboot=k panic=1 pci=off random.trust_cpu=on"},"drives":[],"machine-config":{"vcpu_count":2,"mem_size_mib":${mem}},"network-interfaces":[{"iface_id":"eth0","host_dev_name":"${tap}","guest_mac":"${mac}"}],"logger":{"log_path":"${dir}/firecracker.log","level":"Info","show_level":true,"show_log_origin":true}} EOF "${FIRECRACKER_BIN}" \ --api-sock "${dir}/fc.sock" \ --config-file "${dir}/vm.json" \ > "${dir}/console.log" 2>&1 & echo $! > "${dir}/pid" done
8. Готовность кластера
for ip in "$(node_ip 0)" "$(node_ip 1)" "$(node_ip 2)"; do until ssh "${SSH_OPTS[@]}" "root@${ip}" true >/dev/null 2>&1; do sleep 2 done done for _ in $(seq 1 180); do k3s_exec "kubectl get nodes -o wide > /var/log/create-your-own-talos.nodes 2>&1 || true" >/dev/null 2>&1 || true ready_count="$(server_ssh "awk '\$2==\"Ready\"{c++} END{print c+0}' /var/log/create-your-own-talos.nodes 2>/dev/null || echo 0")" if [[ "${ready_count}" -ge "${NODE_COUNT}" ]]; then break fi sleep 3 done server_ssh "cat /var/log/create-your-own-talos.nodes"
Ожидаемый результат:
linuxkit-00 Ready control-plane,etcd linuxkit-01 Ready <none> linuxkit-02 Ready <none>
9. Smoke-тест
k3s_exec "$(cat <<'EOF' kubectl create namespace smoke --dry-run=client -o yaml | kubectl apply -f - cat <<'YAML' | kubectl apply -f - apiVersion: apps/v1 kind: DaemonSet metadata: name: node-smoke namespace: smoke spec: selector: matchLabels: app: node-smoke template: metadata: labels: app: node-smoke spec: tolerations: - operator: Exists containers: - name: node-smoke image: busybox:1.36 command: ["sh", "-lc", "sleep 3600"] --- apiVersion: apps/v1 kind: Deployment metadata: name: echo namespace: smoke spec: replicas: 2 selector: matchLabels: app: echo template: metadata: labels: app: echo spec: containers: - name: echo image: nginx:1.27-alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: echo namespace: smoke spec: selector: app: echo ports: - port: 80 targetPort: 80 YAML kubectl -n smoke rollout status daemonset/node-smoke --timeout=240s kubectl -n smoke rollout status deployment/echo --timeout=240s kubectl -n kube-system rollout status deployment/coredns --timeout=240s cat <<'YAML' | kubectl apply -f - apiVersion: batch/v1 kind: Job metadata: name: dns-http namespace: smoke spec: backoffLimit: 0 template: spec: restartPolicy: Never containers: - name: dns-http image: busybox:1.36 command: - sh - -lc - | for _ in $(seq 1 30); do nslookup echo.smoke.svc.cluster.local && \ wget -qO- http://echo.smoke.svc.cluster.local >/tmp/echo.html && \ test -s /tmp/echo.html && exit 0 sleep 2 done exit 1 YAML kubectl -n smoke wait --for=condition=complete job/dns-http --timeout=240s kubectl get pods -A -o wide > /var/log/create-your-own-talos.pods 2>&1 kubectl -n smoke get pods -o wide > /var/log/create-your-own-talos.smoke-pods 2>&1 kubectl -n smoke logs job/dns-http > /var/log/create-your-own-talos.smoke-job.log 2>&1 kubectl config view --raw > /var/log/create-your-own-talos.kubeconfig 2>&1 EOF )"
10. Сохранение артефактов
export KUBECONFIG="${RUN_ROOT}/artifacts/kubeconfig.yaml" kubectl get nodes -o wide kubectl get pods -A -o wide
11. Демонтаж
for pid_file in "${RUN_ROOT}"/nodes/*/pid; do [[ -f "${pid_file}" ]] || continue kill "$(cat "${pid_file}")" 2>/dev/null || true done sleep 1 for pid_file in "${RUN_ROOT}"/nodes/*/pid; do [[ -f "${pid_file}" ]] || continue kill -9 "$(cat "${pid_file}")" 2>/dev/null || true done for i in 0 1 2; do ip link del "$(node_tap "${i}")" 2>/dev/null || true done iptables -D FORWARD -i "${BRIDGE_NAME}" -j ACCEPT 2>/dev/null || true iptables -D FORWARD -o "${BRIDGE_NAME}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true iptables -t nat -D POSTROUTING -s "${CIDR}" ! -o "${BRIDGE_NAME}" -j MASQUERADE 2>/dev/null || true ip link set "${BRIDGE_NAME}" down 2>/dev/null || true ip link del "${BRIDGE_NAME}" type bridge 2>/dev/null || true rm -rf "${RUN_ROOT}"
