Centos7.9 使用 Kubeadm 自动化部署 K8S 集群(一个脚本)

目录

一、环境准备

1、硬件准备(虚拟主机)

角色 主机名 ip地址
master k8s-master 192.168.112.10
node k8s-node1 192.168.112.20
node k8s-node2 192.168.112.30

2、操作系统版本

CentOS Linux release 7.9.2009 (Core)

兼容版本包含7.7~7.9

3、硬件配置

每个虚拟主机都需要至少分配2核CPU、3GB以上内存(2C3G)、硬盘20G(VMware Workstation)

4、网络

  • 需要可以访问外网(ping -c 2 www.baidu.com 测试连通性)
  • 集群中所有节点之间网络互通(可以提前设置,脚本里也有相关逻辑)

二、注意点

1、主机命名格式

hostnamectl set-hostname <node-name>

需要与脚本中预设的集群列表一一对应,ip地址建议使用静态ip(nmtui设置)

# 定义主机名与 IP 地址的映射关系
declare -A hosts=(
    ["k8s-master"]="192.168.112.10"
    ["k8s-node1"]="192.168.112.20"
    ["k8s-node2"]="192.168.112.30"
)

2、网络插件 flannel 镜像拉取

主机需要有公钥私钥并添加公钥到 Gitee 的安装设置中的SSH公钥

2.1、主机生成公私钥

ssh-keygen

一路回车即可在 ~/.ssh/ 目录下发现 id_ras(私钥)以及id_rsa.pub(公钥)

2.2、为啥有 Github 还用 Gitee

  • 一方面考虑到 Github 的网络连接不太稳定可能导致克隆仓库失败(git clone [email protected]

  • 另一方面就是 Github 对仓库限制单个文件大小为 50MB(除非使用 Git LFS 管理文件),因为有一个镜像文件的大小超过了 50MB

  • 但其实两者都可以使用(测试过了)

# Github 仓库地址
git clone [email protected]:misakivv/flannel-needs.git

# Gitee 仓库地址
git clone [email protected]:kurosaki01/flannel-needs.git

对应脚本修改

替换为你喜欢使用的

# 克隆 Gitee 仓库
    echo "克隆 Gitee 仓库..."
    git clone [email protected]:kurosaki01/flannel-needs.git

# 检查克隆仓库是否成功
if [ $? -ne 0 ]; then
    echo "克隆仓库失败,可能的原因包括:"
    echo "1. 目标主机没有公钥。"
    echo "2. 公钥没有正确设置到 Gitee 上。"
    echo "3. 克隆仓库时发生网络错误。"
    echo "请检查 SSH 配置和网络连接。"

2.3、将主机公钥添加到 Gitee

2.3.1、复制主机上的公钥

cat ~/.ssh/id_rsa.pub

2.3.2、登录码云

https://gitee.com/

2.3.3、设置 --> 安全设置 --> SSH公钥

2.3.4、添加公钥

标题无所谓

公钥粘贴刚才在主机上复制的内容

每次添加公钥都需要输入账号密码(与 Github 流程一致)

每个节点都需要添加哦,不然部署网络插件时报(ImagePullBackOff)

2.4、将主机公钥添加到 Github

2.4.1、点击头像 --> settings

https://github.com/dashboard

2.4.2、左侧列表中找到 SSH and GPG keys 点击 New SSH key

添加过程与 Gitee 一致,这里不再演示

3、阿里云的镜像加速地址

3.1、镜像加速器地址

这里请在脚本中替换为自己的

https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

3.2、对应脚本的修改位置

# 配置 Docker
echo "配置 Docker..."
cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors": [
    "https://jzjzrggd.mirror.aliyuncs.com",
    "https://dockerproxy.com",
    "https://docker.chenby.cn",
    "https://dockerpull.com",
    "https://dockerhub.jobcher.com",
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub.uuuabc.top",
    "https://mirror.baidubce.com",
    "https://mirror.css.tencentyun.com",
    "https://docker.m.daocloud.io",
    "https://docker.nju.edu.cn",
    "https://hub-mirror.c.163.com",
    "https://dockerhub.azk8s.cn",
    "https://registry.cn-hangzhou.aliyuncs.com"
  ]
}
EOF
systemctl daemon-reload && systemctl restart docker.service
echo "Docker 配置完成。"

4、运行脚本时需要加入集群的主机都在线

因为脚本有两处均需要集群主机在线(开启状态)

4.1、通过 SSH 配置免密登录

# 循环遍历所有节点并复制公钥,排除 master 节点
for node in "${!hosts[@]}"; do
    if [[ ! $node == *"master"* ]]; then
        ip="${hosts[$node]}"
        echo "复制公钥到 $node ($ip)"
        ssh-copy-id -i ~/.ssh/id_rsa.pub root@$ip
    else
        echo "跳过 master 节点 $node"
    fi
done

echo "所有非 master 节点的公钥复制完成。"

4.2、在 master 节点上执行登录各节点执行加入集群命令

# 从 hosts 映射中获取所有 worker 节点
for node in "${!hosts[@]}"; do
    if [[ ! $node == *"-master"* ]]; then
        # 在 worker 节点上直接执行 join 命令
        echo "在节点 $node 上执行 join 命令..."
        ssh root@${hosts[$node]} "$JOIN_COMMAND"
    fi
done

5、输入正确的目标主机名

主要是这块懒得加逻辑判断了

别三台主机运行脚本还能整出排列组合来:)

这里只要输入的主机名在预设的集群列表中脚本就继续运行了,想要优化的可以自行添加逻辑

# 循环提示用户输入主机名,直到输入正确的主机名
while true; do
    read -p "请输入目标主机名 (例如: k8s-master/ks8-node1): " hostname

# 检查输入的主机名是否存在于映射关系中
    if [[ -n "${hosts[$hostname]}" ]]; then
        break
    else
        echo "错误:未知的主机名 '$hostname'。请重新输入。"
    fi
done

6、脚本适用性

  • 因为选的 docker 作为容器运行时,使用限定了 k8s 集群版本必须 < 1.24.x

  • 想要集群版本大于等于 1.24.x 的自己用换成 containerd吧

# 获取用户输入的 Kubernetes 版本
while true; do
    read -p "请输入 Kubernetes 版本号(如 1.23.16,建议不超过 1.24.x): " K8S_VERSION

    # 检查版本号是否符合要求
    if [[ $K8S_VERSION =~ ^1\.([0-9]{1,2})\..* ]] && ((BASH_REMATCH[1] < 24)); then
        break
    else
        echo "错误:版本号不符合要求。建议使用 1.23.x 或之前的版本。请重新输入。"
    fi
done

三、脚本

vim k8s-install.sh
#!/bin/bash

# 定义停止并禁用 firewalld 的函数
stop_and_disable_firewalld() {
    local max_attempts=$1
    local attempt=0

    while [ $attempt -lt $max_attempts ]; do
        systemctl stop firewalld
        systemctl disable firewalld
        if systemctl is-active --quiet firewalld; then
            echo "Attempt $((attempt+1)): firewalld is still active, attempting to stop and disable again."
            attempt=$((attempt+1))
        else
            echo "firewalld has been successfully disabled and stopped."
            return 0
        fi
    done

    echo "firewalld could not be stopped and disabled after $max_attempts attempts."
    echo "This could be due to:"
    echo "1. A service or process is preventing firewalld from being stopped."
    echo "2. There might be a configuration issue with the firewalld service."
    echo "3. There could be a problem with the system's systemd manager."
    return 1
}

# 主逻辑
check_firewalld_status() {
    if systemctl is-active --quiet firewalld; then
        echo "firewalld is currently active, proceeding to stop and disable."
        if ! stop_and_disable_firewalld 3; then
            echo "Failed to stop and disable firewalld."
        fi
    elif systemctl is-enabled --quiet firewalld; then
        echo "firewalld is not active but is enabled, proceeding to disable."
        systemctl disable firewalld
        echo "firewalld has been successfully disabled."
    else
        echo "firewalld is not active and not enabled, no action needed."
    fi
}

# 执行主逻辑
check_firewalld_status

# 定义一个函数来检查 SELinux 状态
check_selinux_status() {
    if getenforce | grep -q "Disabled"; then
        echo "SELinux is already disabled."
        return 0
    else
        echo "SELinux is currently enforcing."
        return 1
    fi
}

# 定义一个函数来更改 SELinux 配置
disable_selinux() {
    # 更改配置文件中的 SELinux 状态
    if sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config; then
        echo "SELinux configuration updated in /etc/selinux/config."
    else
        echo "Failed to update SELinux configuration in /etc/selinux/config."
        return 1
    fi

    # 应用更改
    if setenforce 0; then
        echo "SELinux has been temporarily disabled."
    else
        echo "Failed to disable SELinux temporarily."
        return 1
    fi

    return 0
}

# 主逻辑
if check_selinux_status; then
    echo "No action required."
else
    if disable_selinux; then
        echo "SELinux has been disabled."
    else
        echo "Failed to disable SELinux."
    fi
fi

# 临时关闭 swap
echo "临时关闭 swap..."
swapoff -a

# 永久关闭 swap (注释掉 /etc/fstab 中的 swap 行)
echo "永久关闭 swap..."
sed -ri 's/.*swap.*/#&/' /etc/fstab

# 验证 swap 是否已经被注释掉
echo "验证 swap 是否已经被注释掉..."
cat /etc/fstab | grep swap

# 定义主机名与 IP 地址的映射关系
declare -A hosts=(
    ["k8s-master"]="192.168.112.10"
    ["k8s-node1"]="192.168.112.20"
    ["k8s-node2"]="192.168.112.30"
)

# 循环提示用户输入主机名,直到输入正确的主机名
while true; do
    read -p "请输入目标主机名 (例如: k8s-master/ks8-node1): " hostname

# 检查输入的主机名是否存在于映射关系中
    if [[ -n "${hosts[$hostname]}" ]]; then
        break
    else
        echo "错误:未知的主机名 '$hostname'。请重新输入。"
    fi
done

# 遍历所有主机名与 IP 地址映射关系,并追加到 /etc/hosts 文件中
for node in "${!hosts[@]}"; do
    ip_address=${hosts[$node]}
    host_line="${ip_address} ${node}"

# 检查该行是否已经存在于 /etc/hosts 文件中
    if ! grep -q "$host_line" /etc/hosts; then
# 如果不存在,则追加到 /etc/hosts 文件中
        echo "$host_line" >> /etc/hosts
        echo "已将 '$node' 添加到 /etc/hosts 文件中。"
    fi
done

# 验证用户输入的主机名与对应的 IP 地址是否存在于本地的 /etc/hosts 文件中
ip_address=${hosts[$hostname]}
host_line="${ip_address} ${hostname}"

if grep -q "$host_line" /etc/hosts; then
    echo "主机名 '$hostname' 与对应的 IP 地址 '$ip_address' 已经存在于本地的 /etc/hosts 文件中。"
else
    echo "主机名 '$hostname' 与对应的 IP 地址 '$ip_address' 未能在本地的 /etc/hosts 文件中找到,请检查。"
fi

# 设置 sysctl 参数
echo "设置 sysctl 参数..."
cat > /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system
echo "sysctl 参数设置完成。"

# 安装 ntpdate 并同步时间
echo "安装 ntpdate 并同步时间..."
yum install -y ntpdate
ntpdate time.windows.com
echo "时间同步完成。"

# 安装 wget
echo "安装 wget git curl zsh..."
yum install -y wget.x86_64 git curl zsh
echo "wget 安装完成。"

# 清除原有的 yum 仓库
echo "清除原有的 yum 仓库..."
rm -rf /etc/yum.repos.d/*
echo "原有仓库清除完成。"

# 下载新的仓库文件
echo "下载新的仓库文件..."
wget -O /etc/yum.repos.d/centos7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
wget -O /etc/yum.repos.d/epel-7.repo http://mirrors.aliyun.com/repo/epel-7.repo
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
echo "新的仓库文件下载完成。"

# 安装 Docker
echo "安装 Docker..."
yum install docker-ce -y
systemctl start docker && systemctl enable docker
echo "Docker 安装并启动完成。"

# 配置 Docker
echo "配置 Docker..."
cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors": [
    "https://jzjzrggd.mirror.aliyuncs.com",
    "https://dockerproxy.com",
    "https://docker.chenby.cn",
    "https://dockerpull.com",
    "https://dockerhub.jobcher.com",
    "https://docker.mirrors.ustc.edu.cn",
    "https://hub.uuuabc.top",
    "https://mirror.baidubce.com",
    "https://mirror.css.tencentyun.com",
    "https://docker.m.daocloud.io",
    "https://docker.nju.edu.cn",
    "https://hub-mirror.c.163.com",
    "https://dockerhub.azk8s.cn",
    "https://registry.cn-hangzhou.aliyuncs.com"
  ]
}
EOF
systemctl daemon-reload && systemctl restart docker.service
echo "Docker 配置完成。"

# 添加 Kubernetes 仓库
echo "添加 Kubernetes 仓库..."
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF
echo "Kubernetes 仓库添加完成。"

# 获取用户输入的 Kubernetes 版本
while true; do
    read -p "请输入 Kubernetes 版本号(如 1.23.16,建议不超过 1.24.x): " K8S_VERSION

    # 检查版本号是否符合要求
    if [[ $K8S_VERSION =~ ^1\.([0-9]{1,2})\..* ]] && ((BASH_REMATCH[1] < 24)); then
        break
    else
        echo "错误:版本号不符合要求。建议使用 1.23.x 或之前的版本。请重新输入。"
    fi
done

# 安装指定版本的 Kubernetes 组件
echo "安装指定版本的 Kubernetes 组件..."
yum install kubelet-$K8S_VERSION kubeadm-$K8S_VERSION kubectl-$K8S_VERSION -y

# 启动并启用 kubelet 服务
echo "启动并启用 kubelet 服务..."
systemctl enable kubelet  && systemctl start kubelet

# 检查 kubelet 是否成功启动
if systemctl is-active --quiet kubelet; then
    echo "Kubernetes 组件安装并启动完成。"
else
    echo "错误:kubelet 服务未能成功启动。请检查安装过程是否有误。"
fi

# 判断是否为 master 节点
if [[ $hostname == *"-master"* ]]; then
    echo "检测到当前节点为主节点 '$hostname',执行 kubeadm 初始化..."

# 动态填充 kubeadm init 命令中的参数
    kubeadm init \
        --apiserver-advertise-address=$ip_address \
        --image-repository registry.aliyuncs.com/google_containers \
        --kubernetes-version v$K8S_VERSION \
        --control-plane-endpoint $hostname \
        --service-cidr=172.16.0.0/16 \
        --pod-network-cidr=10.244.0.0/16

# 输出 kubeadm join 命令并保存到变量中
    JOIN_COMMAND=$(kubeadm token create --print-join-command)
# 输出提示信息
    echo "kubeadm 初始化完成,请记录以下 join 命令:"
    echo "$JOIN_COMMAND"
	
# 创建 .kube 目录并复制 admin.conf 文件
    mkdir -p $HOME/.kube
    sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
    sudo chown $(id -u):$(id -g) $HOME/.kube/config

# 设置 KUBECONFIG 环境变量
    export KUBECONFIG=/etc/kubernetes/admin.conf

# 输出提示信息
    echo "为了开始使用您的集群,请执行以下命令:"
    echo "source <(kubectl completion bash)"
	
# 克隆 Gitee 仓库
    echo "克隆 Gitee 仓库..."
    git clone [email protected]:kurosaki01/flannel-needs.git
	
# 检查克隆仓库是否成功
if [ $? -ne 0 ]; then
    echo "克隆仓库失败,可能的原因包括:"
    echo "1. 目标主机没有公钥。"
    echo "2. 公钥没有正确设置到 Gitee 上。"
    echo "3. 克隆仓库时发生网络错误。"
    echo "请检查 SSH 配置和网络连接。"
else
    # 进入克隆的仓库目录
    echo "进入 flannel-needs 目录..."
    cd flannel-needs || {
        echo "进入 flannel-needs 目录失败,可能的原因包括:"
        echo "1. 克隆仓库未成功创建目录。"
        echo "2. 当前目录不存在 flannel-needs 子目录。"
        echo "请检查克隆仓库是否成功。"
    }
fi

# 加载 Docker 镜像
    echo "加载 Docker 镜像..."
    docker load -i lizhenliang-flannel-v0.11.0-amd64.tar
    docker load -i ranche-mirrored-flannelcni-flannel-cni-plugin.tar

# 应用 Flannel 配置
    echo "应用 Flannel 配置..."
    kubectl apply -f flannel.yaml

# 输出提示信息
    echo "Flannel 配置已应用,网络插件已准备好。"
    
# 循环遍历所有节点并复制公钥,排除 master 节点
for node in "${!hosts[@]}"; do
    if [[ ! $node == *"master"* ]]; then
        ip="${hosts[$node]}"
        echo "复制公钥到 $node ($ip)"
        ssh-copy-id -i ~/.ssh/id_rsa.pub root@$ip
    else
        echo "跳过 master 节点 $node"
    fi
done

echo "所有非 master 节点的公钥复制完成。"

# 从 hosts 映射中获取所有 worker 节点
for node in "${!hosts[@]}"; do
    if [[ ! $node == *"-master"* ]]; then
        # 在 worker 节点上直接执行 join 命令
        echo "在节点 $node 上执行 join 命令..."
        ssh root@${hosts[$node]} "$JOIN_COMMAND"
    fi
done

# 从预设的集群列表中获取预期的节点数量
EXPECTED_COUNT=${#hosts[@]}

# 获取当前已加入集群的节点数量
CURRENT_COUNT=$(kubectl get nodes | grep -v NAME | wc -l)

# 获取当前已加入集群的节点名称
CURRENT_NODES=$(kubectl get nodes | awk 'NR>1{print $1}')

# 循环检查直到所有预期的节点都已加入集群并且状态为 Ready
while [ $CURRENT_COUNT -ne $EXPECTED_COUNT ]; do
    echo "当前已加入集群的节点数量 ($CURRENT_COUNT) 与预期数量 ($EXPECTED_COUNT) 不匹配,请等待..."
    sleep 10  # 等待10秒后再次检查
    CURRENT_COUNT=$(kubectl get nodes | grep -v NAME | wc -l)
    CURRENT_NODES=$(kubectl get nodes | awk 'NR>1{print $1}')
done

# 检查所有节点的状态是否为 Ready=True
while true; do
    NODES_STATUS=$(kubectl get nodes -o jsonpath='{range .items[*]}{.status.conditions[-1:].type}{"="}{.status.conditions[-1:].status}{"\n"}{end}')
    
    # 使用数组存储每个节点的状态
    IFS=$'\n' read -d '' -r -a nodeStatusArray <<< "$NODES_STATUS"
    
    # 标记所有节点是否都 Ready
    allReady=true
    
    for status in "${nodeStatusArray[@]}"; do
        if [[ $status != "Ready=True" ]]; then
            allReady=false
            break
        fi
    done
    
    if $allReady; then
        echo "所有节点的状态均为 Ready,集群安装成功。"
        break
    else
        echo "集群中有节点状态不是 Ready,请等待..."
        sleep 10  # 等待10秒后再次检查
    fi
done

# 输出当前集群节点状态
echo "检查集群节点状态..."
kubectl get nodes

else
    echo "检测到当前节点为 worker 节点 '$hostname',执行命令..."

# "检测到当前节点为 worker 节点 '$hostname',执行命令..."

# 克隆 Gitee 仓库
    echo "克隆 Gitee 仓库..."
    git clone [email protected]:kurosaki01/flannel-needs.git

# 检查克隆仓库是否成功
if [ $? -ne 0 ]; then
    echo "克隆仓库失败,可能的原因包括:"
    echo "1. 目标主机没有公钥。"
    echo "2. 公钥没有正确设置到 Gitee 上。"
    echo "3. 克隆仓库时发生网络错误。"
    echo "请检查 SSH 配置和网络连接。"
    
else
    # 进入克隆的仓库目录
    echo "进入 flannel-needs 目录..."
    cd flannel-needs || {
        echo "进入 flannel-needs 目录失败,可能的原因包括:"
        echo "1. 克隆仓库未成功创建目录。"
        echo "2. 当前目录不存在 flannel-needs 子目录。"
        echo "请检查克隆仓库是否成功。"
    }
fi

# 加载 Docker 镜像
    echo "加载 Docker 镜像..."
    docker load -i lizhenliang-flannel-v0.11.0-amd64.tar
    docker load -i ranche-mirrored-flannelcni-flannel-cni-plugin.tar

# 输出提示信息
    echo "Docker 镜像已加载,worker 节点准备完毕。"
	
# 提示用户在 master 节点上检查集群状态
    echo "请在 master 节点上执行 'kubectl get nodes' 来检查集群状态。"
fi

四、实例演示

1、硬件准备

2、**,启动!

hostnamectl set-hostname <hostname>

bash

hostname

3、创建脚本文件(CV大法)

vim k8s-install.sh

4、运行脚本

bash k8s-install.sh

5、判断并关闭完firewalld、selinux、swap 后输入目标主机名

输入 k8s-master
输入 k8s-node1
输入 k8s-node2

6、master节点将预设的集群列表一个个追加到/etc/hosts文件中,所有节点执行判断逻辑

7、所有节点设置 sysctl 参数,安装 ntpdate 并同步时间

8、安装 wget git curl zsh...(pass)

不截图,太长了

9、清除原有的 yum 仓库,下载新的仓库文件

当然了,涉及到 rm 删除操作可以添加备份的逻辑

我就懒得搞(快照)

10、安装,配置 Docker 、添加 Kubernetes 仓库(pass)

硬编码一般没什么问题

11、输入想要安装的 k8s 版本

此脚本只适用 k8s < 1.24.0 版本的

想要高版本自行修改

12、安装三件套kubelet、kubeadm、kubectl(pass)

13、node节点:克隆镜像仓库

输入 yes
输入 yes

14、加载 Docker 镜像

k8s-node1: 我滴使命完成啦
k8s-node2: 我滴使命也完成啦

15、master 节点 kubeadm 初始化好了以后保存 kubeadm join 命令为环境变量、创建 .kube 目录并复制 admin.conf 文件以及设置 KUBECONFIG 环境变量、kubectl 设置命令补全

16、克隆镜像仓库,部署网络插件

17、master 节点配置免密登录

需要输入 k8s-node1、k8s-node2 的登录密码

18、通过 SSH 登录 node 节点执行 join 操作

k8s-master:done

19、每十秒检查集群状态,都为 Ready 即可退出脚本

20、小坑:node节点是需要 admin.conf 文件并且设置 export KUBECONFIG=/etc/kubernetes/admin.conf 环境变量才可以使用kubectl命令操作集群的

解决办法详见这篇:centos7.9部署k8s的几种方式

想优化脚本的自己优化吧,我后续会把坑补上并且设置花里胡哨的 oh-my-zsh + p10k

毕竟这脚本写的还是比较拉的