1.打包所有k8s离线需要的镜像及RPM包
查看主页资源下载即可
包含所有,拷入服务器无需解压,脚本自会解压

2.编辑脚本
bash
vim deploy-k8s.sh
以下内容
bash
#!/bin/bash
#===============================================================================
# K8s v1.30.14 All-in-One 离线部署脚本
# 整合所有踩坑修复:
# 1. kubelet drop-in 文件缺失(核心问题)
# 2. 镜像 unpack 问题
# 3. containerd 配置
# 4. CNI 网络配置
# 5. 自动查找并解压离线包
# 6. 兼容 RPM 为空时从 binaries/ 安装
# 7. 【修复】镜像导入不再按文件名筛选,全量导入避免遗漏
#===============================================================================
set -e
# ========================= 配置区 =========================
K8S_VERSION="v1.30.14"
IMAGE_REPO="registry.aliyuncs.com/google_containers"
SERVICE_CIDR="10.96.0.0/12"
POD_CIDR="10.244.0.0/16"
CRI_SOCKET="unix:///var/run/containerd/containerd.sock"
OFFLINE_BASE="/root"
OFFLINE_DIR="/root/k8s-offline-local"
# ==========================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%H:%M:%S') $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $1"; }
log_step() {
echo ""
echo -e "${CYAN}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ $1${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════╝${NC}"
}
#===============================================================================
# 第0步:自动查找并解压离线包
#===============================================================================
log_step "第0步:自动查找并解压离线包"
if [[ -d "$OFFLINE_DIR" && -f "$OFFLINE_DIR/MANIFEST.txt" ]]; then
log_info "已存在解压目录 $OFFLINE_DIR,跳过解压"
else
ARCHIVE="$(find "$OFFLINE_BASE" -maxdepth 1 -name 'k8s-offline-local-*.tar.gz' -type f 2>/dev/null | sort -r | head -1)"
if [[ -z "$ARCHIVE" ]]; then
log_error "未找到离线包!"
log_error "请将 k8s-offline-local-*.tar.gz 放到 ${OFFLINE_BASE}/ 下"
exit 1
fi
log_info "找到离线包: $(basename "$ARCHIVE")"
log_info "文件大小: $(du -h "$ARCHIVE" | awk '{print $1}')"
log_info "正在解压..."
tar -xzf "$ARCHIVE" -C "$OFFLINE_BASE"
EXTRACTED="$(find "$OFFLINE_BASE" -maxdepth 1 -type d -name 'k8s-offline-local*' ! -name '*.tar.gz' 2>/dev/null | sort -r | head -1)"
if [[ -n "$EXTRACTED" && "$EXTRACTED" != "$OFFLINE_DIR" ]]; then
rm -rf "$OFFLINE_DIR"
mv "$EXTRACTED" "$OFFLINE_DIR"
fi
if [[ ! -f "$OFFLINE_DIR/MANIFEST.txt" ]]; then
log_error "解压后未找到 MANIFEST.txt,离线包可能不完整"
exit 1
fi
log_info "解压完成 → $OFFLINE_DIR"
fi
log_info "离线包内容:"
ls -la "$OFFLINE_DIR/"
#===============================================================================
# 第1步:前置检查
#===============================================================================
log_step "第1步:前置检查"
if [[ $EUID -ne 0 ]]; then
log_error "请以 root 用户运行此脚本"
exit 1
fi
MASTER_IP="$(hostname -I | awk '{print $1}')"
log_info "主机名: $(hostname)"
log_info "IP地址: $MASTER_IP"
log_info "系 统: $(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)"
log_info "内 核: $(uname -r)"
log_info "CPU: $(nproc) 核"
log_info "内存: $(free -h | awk '/Mem:/{print $2}')"
#===============================================================================
# 第2步:系统基础配置
#===============================================================================
log_step "第2步:系统基础配置"
swapoff -a
sed -i '/swap/s/^/#/' /etc/fstab
log_info "swap 已关闭"
setenforce 0 2>/dev/null || true
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config 2>/dev/null || true
sed -i 's/^SELINUX=permissive/SELINUX=disabled/' /etc/selinux/config 2>/dev/null || true
log_info "SELinux 已关闭"
systemctl stop firewalld 2>/dev/null || true
systemctl disable firewalld 2>/dev/null || true
log_info "防火墙已关闭"
cat > /etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay 2>/dev/null || true
modprobe br_netfilter 2>/dev/null || true
cat > /etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
vm.swappiness = 0
EOF
sysctl --system > /dev/null 2>&1
log_info "内核模块 & sysctl 已配置"
timedatectl set-timezone Asia/Shanghai 2>/dev/null || true
log_info "时区已设为 Asia/Shanghai"
#===============================================================================
# 第3步:安装核心组件(RPM 优先,fallback 到 binaries/)
#===============================================================================
log_step "第3步:安装核心组件"
INSTALLED_FROM=""
# 方式1:尝试 RPM 安装
if [[ -d "$OFFLINE_DIR/rpms" ]] && ls "$OFFLINE_DIR/rpms"/*.rpm &>/dev/null; then
log_info "检测到 RPM 包,使用 rpm 安装..."
rpm -ivh --force --nodeps "$OFFLINE_DIR/rpms"/*.rpm 2>&1 | tail -5
INSTALLED_FROM="rpms"
log_info "RPM 安装完成"
else
log_warn "RPM 目录为空或不存在,尝试从 binaries/ 安装..."
fi
# 方式2:从 binaries/ 安装(RPM 为空或未成功时的 fallback)
if [[ -d "$OFFLINE_DIR/binaries" ]]; then
BIN_DIR="$OFFLINE_DIR/binaries"
log_info "从 $BIN_DIR 安装二进制文件..."
# kubelet / kubeadm / kubectl → /usr/bin/
for bin in kubelet kubeadm kubectl; do
if [[ -f "$BIN_DIR/$bin" ]]; then
cp -f "$BIN_DIR/$bin" /usr/bin/$bin
chmod +x /usr/bin/$bin
log_info " ✓ $bin → /usr/bin/$bin"
fi
done
# containerd / ctr / crictl / runc → /usr/local/bin/ 或 /usr/bin/
for bin in containerd ctr crictl runc containerd-shim-runc-v2; do
if [[ -f "$BIN_DIR/$bin" ]]; then
cp -f "$BIN_DIR/$bin" /usr/local/bin/$bin
chmod +x /usr/local/bin/$bin
log_info " ✓ $bin → /usr/local/bin/$bin"
fi
done
# 确保 PATH 包含 /usr/local/bin
export PATH="/usr/local/bin:$PATH"
echo 'export PATH="/usr/local/bin:$PATH"' >> /etc/profile.d/k8s-path.sh
chmod +x /etc/profile.d/k8s-path.sh
INSTALLED_FROM="binaries"
log_info "二进制安装完成"
fi
# 安装 systemd unit 文件(如果离线包里有)
if [[ -d "$OFFLINE_DIR/systemd" ]]; then
log_info "安装 systemd unit 文件..."
find "$OFFLINE_DIR/systemd" -name '*.service' -exec cp -f {} /etc/systemd/system/ \; 2>/dev/null || true
systemctl daemon-reload
log_info "systemd unit 文件已安装"
fi
# ★ 验证关键命令 ★
MISSING=""
for cmd in kubelet kubeadm kubectl containerd ctr crictl; do
if ! command -v "$cmd" &>/dev/null; then
MISSING="$MISSING $cmd"
fi
done
if [[ -n "$MISSING" ]]; then
log_error "以下命令未找到:$MISSING"
log_error "请检查离线包中 rpms/ 或 binaries/ 是否包含这些文件"
log_error "当前 binaries/ 内容:"
ls -la "$OFFLINE_DIR/binaries/" 2>/dev/null || echo " (目录不存在)"
exit 1
fi
log_info "安装来源: $INSTALLED_FROM"
log_info "kubelet/kubeadm/kubectl/containerd/ctr/crictl 均已就绪 ✓"
#===============================================================================
# 第4步:安装 CNI plugins
#===============================================================================
log_step "第4步:安装 CNI plugins"
mkdir -p /opt/cni/bin
if [[ -d "$OFFLINE_DIR/cni-plugins" ]] && ls "$OFFLINE_DIR/cni-plugins"/* &>/dev/null; then
cp -f "$OFFLINE_DIR/cni-plugins"/* /opt/cni/bin/
chmod +x /opt/cni/bin/*
log_info "CNI plugins 已安装: $(ls /opt/cni/bin/ | wc -l) 个"
ls /opt/cni/bin/ | tr '\n' ' '
echo ""
else
log_warn "cni-plugins 目录为空,跳过"
fi
#===============================================================================
# 第5步:配置 containerd
#===============================================================================
log_step "第5步:配置 containerd"
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
# sandbox 镜像 → 阿里云
sed -i "s|sandbox_image = .*|sandbox_image = \"${IMAGE_REPO}/pause:3.9\"|" /etc/containerd/config.toml
# cgroup driver → systemd
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# snapshotter → overlayfs
sed -i 's/snapshotter = "native"/snapshotter = "overlayfs"/' /etc/containerd/config.toml 2>/dev/null || true
systemctl daemon-reload
systemctl enable containerd
systemctl restart containerd
sleep 3
if systemctl is-active --quiet containerd; then
log_info "containerd 运行正常 ✓"
crictl info > /dev/null 2>&1 && log_info "crictl 连接正常 ✓"
else
log_error "containerd 启动失败!"
journalctl -u containerd --no-pager -n 20
exit 1
fi
#===============================================================================
# 第6步:导入容器镜像 ★★★ 修复:全量导入,不再按文件名筛选 ★★★
#===============================================================================
log_step "第6步:导入容器镜像"
IMAGE_DIR="$OFFLINE_DIR/images"
IMPORTED=0
SKIPPED=0
FAILED=0
if [[ -d "$IMAGE_DIR" ]] && ls "$IMAGE_DIR"/*.tar &>/dev/null; then
for tar_file in "$IMAGE_DIR"/*.tar; do
filename="$(basename "$tar_file")"
# 跳过 sha256 digest 命名的重复文件(与 tag 命名的是同一镜像)
if echo "$filename" | grep -qE "_sha256_"; then
SKIPPED=$((SKIPPED + 1))
continue
fi
log_info "导入: $filename"
if ctr -n k8s.io images import "$tar_file" > /dev/null 2>&1; then
IMPORTED=$((IMPORTED + 1))
else
log_warn "导入失败: $filename"
FAILED=$((FAILED + 1))
fi
done
else
log_warn "images 目录为空或不存在"
fi
log_info "导入完成: 成功=$IMPORTED 跳过=$SKIPPED 失败=$FAILED"
log_info "当前 k8s 镜像列表:"
ctr -n k8s.io images ls -q 2>/dev/null | grep -v "^sha256:" | sort | while read -r img; do
echo " $img"
done
#===============================================================================
# 第7步:创建 kubelet drop-in ★★★ 核心修复 ★★★
#===============================================================================
log_step "第7步:创建 kubelet drop-in(核心修复)"
mkdir -p /etc/systemd/system/kubelet.service.d
cat > /etc/systemd/system/kubelet.service.d/10-kubeadm.conf <<'EOF'
# Note: This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
EnvironmentFile=-/etc/sysconfig/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
EOF
systemctl daemon-reload
systemctl enable kubelet
log_info "kubelet drop-in 已创建 ✓"
#===============================================================================
# 第8步:清理旧状态(支持重复执行)
#===============================================================================
log_step "第8步:清理旧状态"
if kubectl get nodes &>/dev/null 2>&1; then
log_warn "检测到已有集群,执行 kubeadm reset..."
fi
kubeadm reset -f --cri-socket "$CRI_SOCKET" 2>/dev/null || true
rm -rf /var/lib/etcd/*
rm -rf /etc/kubernetes/manifests
rm -rf /etc/kubernetes/pki
rm -rf /var/lib/kubelet/config.yaml
rm -rf /var/lib/kubelet/kubeadm-flags.env
rm -rf /etc/cni/net.d/*
rm -rf "$HOME/.kube"
echo 1 > /proc/sys/net/ipv4/ip_forward
systemctl restart containerd
sleep 2
systemctl restart kubelet 2>/dev/null || true
sleep 3
log_info "清理完成 ✓"
#===============================================================================
# 第9步:kubeadm init
#===============================================================================
log_step "第9步:kubeadm init"
log_info "参数:"
echo " K8s版本: $K8S_VERSION"
echo " 镜像仓库: $IMAGE_REPO"
echo " Service CIDR: $SERVICE_CIDR"
echo " Pod CIDR: $POD_CIDR"
echo " CRI Socket: $CRI_SOCKET"
echo ""
kubeadm init \
--kubernetes-version="${K8S_VERSION}" \
--image-repository="${IMAGE_REPO}" \
--service-cidr="${SERVICE_CIDR}" \
--pod-network-cidr="${POD_CIDR}" \
--cri-socket="${CRI_SOCKET}" \
--ignore-preflight-errors=all \
2>&1 | tee /tmp/kubeadm-init.log
if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
log_error "kubeadm init 失败!查看日志: /tmp/kubeadm-init.log"
exit 1
fi
log_info "kubeadm init 成功 ✓"
#===============================================================================
# 第10步:配置 kubectl
#===============================================================================
log_step "第10步:配置 kubectl"
mkdir -p "$HOME/.kube"
cp -f /etc/kubernetes/admin.conf "$HOME/.kube/config"
chown "$(id -u):$(id -g)" "$HOME/.kube/config"
export KUBECONFIG=/etc/kubernetes/admin.conf
cat > /etc/profile.d/kubectl.sh <<'EOF'
export KUBECONFIG=/etc/kubernetes/admin.conf
alias k='kubectl'
source <(kubectl completion bash) 2>/dev/null || true
complete -o default -F __start_kubectl k 2>/dev/null || true
EOF
chmod +x /etc/profile.d/kubectl.sh
log_info "kubectl 配置完成 ✓"
#===============================================================================
# 第11步:部署 Flannel 网络 ★★★ 修复:移除冗余的单镜像导入 ★★★
#===============================================================================
#===============================================================================
# 第11步:部署 Calico 网络
#===============================================================================
log_step "第11步:部署 Calico 网络"
CALICO_YAML="$OFFLINE_DIR/configs/calico.yaml"
if [[ -f "$CALICO_YAML" ]]; then
log_info "找到 Calico YAML: $CALICO_YAML"
log_info "执行 kubectl apply..."
kubectl apply -f "$CALICO_YAML"
log_info "等待 Calico & CoreDNS 就绪(最多 180 秒)..."
WAIT=0
MAX_WAIT=180
while [[ $WAIT -lt $MAX_WAIT ]]; do
READY="$(kubectl get pods -A --field-selector=status.phase=Running 2>/dev/null | grep -cE '(coredns|calico)' || true )"
TOTAL="$(kubectl get pods -A 2>/dev/null | grep -cE '(coredns|calico)' || true )"
if [[ "$READY" -ge 3 && "$READY" == "$TOTAL" ]]; then
log_info "Calico & CoreDNS 全部就绪 ✓ (${WAIT}s)"
break
fi
sleep 5
WAIT=$((WAIT + 5))
printf "\r 已等待 %ds ... (Running: %s/%s)" "$WAIT" "$READY" "$TOTAL"
done
echo ""
if [[ $WAIT -ge $MAX_WAIT ]]; then
log_warn "超时!部分 Pod 可能仍在启动中,请手动检查"
fi
else
log_warn "未找到 $CALICO_YAML,CoreDNS 将无法启动"
fi
#===============================================================================
# 第12步:验证集群
#===============================================================================
log_step "第12步:验证集群"
echo ""
log_info "--- 节点状态 ---"
kubectl get nodes -o wide 2>/dev/null || true
echo ""
log_info "--- 全部 Pod ---"
kubectl get pods -A -o wide 2>/dev/null || true
echo ""
log_info "--- 组件健康 ---"
kubectl get componentstatuses 2>/dev/null || true
echo ""
log_info "--- 集群信息 ---"
kubectl cluster-info 2>/dev/null || true
echo ""
log_info "--- API Server 连通性 ---"
kubectl get --raw /healthz 2>/dev/null && log_info "API Server 健康 ✓" || log_warn "API Server 异常"
#===============================================================================
# 🎉 完成
#===============================================================================
log_step "🎉 K8s $K8S_VERSION 部署完成"
JOIN_CMD="$(kubeadm token create --print-join-command 2>/dev/null || echo "")"
cat <<EOF
┌─────────────────────────────────────────────┐
│ 部 署 信 息 汇 总 │
├─────────────────────────────────────────────┤
│ K8s 版本: $K8S_VERSION
│ Master IP: $MASTER_IP
│ Pod CIDR: $POD_CIDR
│ Service CIDR: $SERVICE_CIDR
│ 网络插件: Flannel (VXLAN)
│ 容器运行时: containerd
├─────────────────────────────────────────────┤
│ kubectl 使用:
│ export KUBECONFIG=/etc/kubernetes/admin.conf
│ kubectl get nodes -o wide
│ kubectl get pods -A
├─────────────────────────────────────────────┤
│ 添加 Worker 节点:
│ $JOIN_CMD
└─────────────────────────────────────────────┘
EOF
echo "============================================================"
log_info "部署日志已保存至 /tmp/kubeadm-init.log"
echo "============================================================"
3.执行脚本
bash deploy-k8s.sh