☰
K8s与Harbor脚本化部署:一条命令搞定集群初始化与镜像仓库配置
2026/10/10 3:25:52 网站建设 项目流程

手动部署过k8s集群的人,大多体会过那种“复制粘贴半小时,排错排到凌晨”的酸爽。k8s加harbor的组合更是把复杂度直接拉满:证书签发、私有仓库推送、containerd配置、kubeadm初始化、镜像拉取认证,每一步都是坑。这篇博文想分享的,就是一套可以直接拿去用的脚本化安装方案:一条命令装好Harbor,一条命令初始化K8s节点,再把两者之间的证书分发和镜像拉取彻底打通。适合在内网搭环境、做测试、搞交付验证的朋友参考,也适合刚接触容器编排的人理解整套链路是怎么转起来的。

1. 为什么要脚本化部署k8s和harbor

1.1 手动部署的痛点

我最早搭k8s的时候,是照着官方文档一步步敲的。master节点装完,还要去worker节点重复敲至少40条命令:关swap、加载内核模块、装containerd、改config.toml、装kubeadm、kubeadm join,每次都一样,又必须一模一样。只要有一步手滑,比如漏了SystemdCgroup = true,后面Pod会全部以CrashLoopBackOff状态躺给你看。

Harbor这边也没有省事。要改harbor.yml的hostname、要生成证书、要确保docker compose能跑起来。一旦IP变化,证书又得重新签。这些东西如果只做一次,手工还能忍受;如果是三节点、五节点的集群,人在重复劳动下的出错率会指数级上升。我当时在交付现场吃过亏:两台节点的配置时间差了几十分钟,结果因为某台机器内核参数没同步,kubelet始终起不来,排查了一整晚。

脚本化之后,同样的操作变成“传文件-执行-看输出”,整个过程是确定性的。这也是我后来坚持把部署流程固化成脚本的根本原因。

1.2 三段式脚本架构

我的方案把整个部署拆成三个独立脚本:

  • install_harbor.sh:负责Harbor节点,生成证书、写配置、启动服务。
  • init_node.sh:负责所有k8s节点的基础环境初始化,包括内核参数、containerd、kubeadm/kubelet/kubectl。
  • init_master.sh:在master节点执行,完成kubeadm init和CNI插件安装。

三段式的好处很直接——故障边界清晰。每一步都能单独重跑,幂等性也能逐步验证。脚本内部用set -euo pipefail,任何一步失败就立即退出并打日志,定位问题不用靠肉眼翻几百行命令记录。

1.3 版本选型

部署前先定版本,这是我在多次踩坑后学到的经验。k8s 1.29 + containerd 1.7.x + Harbor 2.9.x,这套组合在我常用的环境里验证得比较稳。k8s从1.24开始,dockershim被正式移除,运行时基本就用containerd,所以部署时要确保cri插件和kubelet的版本匹配。Harbor离线安装包自带docker镜像,装起来不依赖外网。我们内网环境没有外部网络,离线包是必选项。

建议所有机器提前规划好IP和主机名,比如:

角色IP主机名
Harbor192.168.209.133harbor.local
K8s Master192.168.209.130master1
K8s Worker192.168.209.131node1
K8s Worker192.168.209.132node2

主机名如果后期能统一,集群内部的DNS解析和证书管理都会省很多事。

2. Harbor脚本化安装全流程

2.1 前置依赖准备

Harbor的离线包解压后,目录里有install.sh、docker-compose相关模板、harbor.yml.tmpl等文件。本质上它依赖docker和docker compose,以及openssl(生成自签证书时用)。

Harbor 2.x版本直接用docker compose插件就行,不再强制依赖docker-compose v1。我习惯在部署脚本里先做一次探测:

if ! command -v docker >/dev/null 2>&1; then echo "docker is required, check failed" exit 1 fi if ! docker compose version >/dev/null 2>&1; then echo "docker compose plugin is required" exit 1 fi

另外,离线包和安装脚本要解压到磁盘空间充足的分区,Harbor的数据目录默认在/data,装之前看一下df -h,至少留够几十GB,镜像多了以后这个目录会飞速膨胀。磁盘满了之后的表现很隐蔽:Harbor的core容器反复重启,但日志里看不到明显的OOM信息,只有不停刷数据库连接错误。

2.2 证书生成和harbor.yml配置

Harbor默认会启用HTTP,但docker/containerd这类客户端默认走HTTPS。直接使用HTTP当然可以,但每次配置都要额外加insecure-registries之类的东西,内网环境里容易埋雷。所以我习惯在脚本里直接用openssl生成CA和服务器证书,让Harbor以HTTPS方式对外服务。

服务器证书需要包含harbor的hostname和IP的SAN,否则客户端校验时会提示证书不匹配:

# 生成CA私钥和证书 openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -subj "/CN=harbor.local" -days 3650 -out ca.crt # 生成服务器私钥和CSR openssl genrsa -out server.key 2048 openssl req -new -key server.key -subj "/CN=harbor.local" -out server.csr # 使用SAN扩展,把IP和域名都写进去 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 3650 \ -extfile <(printf "subjectAltName=IP:192.168.209.133,DNS:harbor.local")

harbor.yml里的关键配置如下,模板文件提供了几乎所有字段,我每次只改必填项:

hostname: 192.168.209.133 http: port: 80 https: port: 443 certificate: /opt/harbor/certs/server.crt private_key: /opt/harbor/certs/server.key harbor_admin_password: Harbor12345 database: password: root123 data_volume: /data/harbor

注意:hostname不要随便填localhost,后面客户端连接、证书校验都会依赖这个值。

2.3 install.sh执行与验证

配置写好后直接执行官方脚本:

cd /opt/harbor ./install.sh --with-trivy

加--with-trivy能把镜像漏洞扫描组件一并启动。如果不需要,去掉这个参数。安装过程会先加载离线镜像,再启动容器,日志会输出每个容器的状态。验证就两件事:docker ps看所有Harbor容器都是Up状态,再用curl访问API:

curl -u admin:Harbor12345 https://192.168.209.133/api/v2.0/systeminfo

能返回类似{"storage":[{...}]}的内容,说明Harbor已经正常工作了。这一步不要跳过,很多看似“装了”实际上端口没监听的情况,都靠这个请求暴露出问题。

如果是在已有的docker环境里装Harbor,还要注意docker daemon的storage driver。Harbor容器对overlay2支持得最好,如果daemon用的是vfs或其他driver,启动时大概率会报磁盘或分层相关错误,直接改docker的storage-driver配置比在Harbor这边绕路更实际。

3. K8s集群脚本化部署全流程

3.1 节点基础环境初始化

所有k8s节点上,第一步是环境初始化。脚本里必须完成这几件事,缺一个后面都会出幺蛾子:

  • 关闭swap:kubelet默认要求关闭swap。
  • 加载br_netfilter模块:让iptables能处理桥接流量。
  • 配置sysctl:net.bridge.bridge-nf-call-iptables等参数需要为1。
  • 安装containerd并配置cgroup驱动。

基础环境脚本的大致结构:

#!/usr/bin/env bash set -euo pipefail # 关闭swap swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab # 加载内核模块 modprobe br_netfilter cat <<EOF >/etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --system # 安装containerd apt-get update && apt-get install -y containerd mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml

这里有个细节:sed -i '/swap/s/^/#/' /etc/fstab只是把fstab里的swap行注释掉,如果这台机器后面重启,swap不会自动挂载,符合kubelet的要求。但如果你的环境里fstab的swap行写法比较特殊(比如带了UUID注释),建议在脚本里先grep确认一下是否真正被注释掉,不要想当然。

3.2 containerd配置的细节

containerd默认配置里的SystemdCgroup是false,而kubelet默认使用systemd cgroup驱动。两者不一致,节点上的Pod就会反复启动失败。这一行必须改成true:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] systemd_cgroup = true

同时,sandbox_image默认指向registry.k8s.io/pause,这个地址在部分内网环境根本拉不动。我习惯提前替换成可访问的镜像地址,比如:

sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"

再用crictl确认一下运行时可用:

crictl --runtime-endpoint unix:///var/run/containerd/containerd.sock version

输出正常后,基础环境才算真正就绪。这一步看起来简单,但它决定了后面所有Pod能不能调度、镜像能不能拉下来。

还有一个容易被忽略的地方:不要只改config.toml,还要确认containerd的systemd unit没有额外参数覆盖配置。有些发行版会在启动参数里追加--config指向别的路径,导致你改的文件根本没生效。用systemctl cat containerd看一眼ExecStart,确认它用的是/etc/containerd/config.toml。

3.3 kubeadm初始化与CNI网络插件

基础环境就绪后,装kubeadm、kubelet、kubectl三项,版本必须一致,不能用apt自动给你拉的最新版。用apt直接指定版本:

apt-get install -y kubelet=1.29.0-* kubeadm=1.29.0-* kubectl=1.29.0-*

初始化命令的关键参数我也固定下来:

kubeadm init \ --apiserver-advertise-address=192.168.209.130 \ --image-repository=registry.aliyuncs.com/google_containers \ --kubernetes-version=v1.29.0 \ --pod-network-cidr=10.244.0.0/16 \ --cri-socket=unix:///var/run/containerd/containerd.sock

--image-repository换成可用的镜像仓库地址,是为了在无外网或外网访问慢的环境里让初始化过程不至于卡死。--pod-network-cidr必须和后面安装的CNI插件保持同一网段,用flannel就用10.244.0.0/16,用calico通常用192.168.0.0/16。初始化成功后执行:

mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

最后装CNI。内网环境建议先把flannel或calico的yaml下载到本地再apply,避免现场网络不通:

kubectl apply -f kube-flannel.yml

等几分钟,kubectl get nodes就能看到master处于Ready状态。worker节点用kubeadm join生成的token加入即可,token过期就重新生成:

kubeadm token create --print-join-command

关于CNI,我多说一句:如果你的环境里需要限制Pod之间的通信策略,calico是更好的选择,它的NetworkPolicy支持比flannel完整得多。如果只是需要一个能跑通的基础集群,flannel部署更轻、排错更少。选型时别跟风,看你的实际需求。

4. 打通Harbor与K8s:证书分发和镜像拉取配置

4.1 证书分发到所有节点

Harbor用HTTPS提供镜像服务后,k8s各节点在拉取镜像时,需要信任我们自签的CA证书。做法是把Harbor节点上生成的ca.crt分发到每个节点。

对于docker客户端,放到/etc/docker/certs.d/192.168.209.133/ca.crt,docker就会在访问这个地址时自动使用该CA做校验。

对于containerd运行时,情况稍有不同。containerd不会自动读取/etc/docker/certs.d,需要把ca.crt放到系统的信任区(比如/etc/ssl/certs/ca-certificates.crt),或者修改config.toml给这个registry指定ca。我常用的方式是直接把ca.crt追加进系统CA信任列表:

cp harbor-ca.crt /usr/local/share/ca-certificates/harbor-ca.crt update-ca-certificates

分发证书用scp或ansible都行。这一步千万别漏掉worker节点,否则Pod调度到node1上,镜像拉取会直接报unknown authority。更麻烦的是,这种报错只会在Pod被调度到特定节点时才出现,看起来像随机偶发,实际上就是证书没分发到位。

4.2 配置镜像仓库的拉取凭证

Harbor创建好项目后,需要创建一个Robot账号或者普通用户,专门给k8s集群拉镜像用。在kubectl里创建secret:

kubectl create secret docker-registry harbor-registry-secret \ --docker-server=192.168.209.133 \ --docker-username=robot$project \ --docker-password=xxx \ --docker-email=robot@example.local

然后在Deployment的模板里加上imagePullSecrets:

spec: imagePullSecrets: - name: harbor-registry-secret containers: - name: app image: 192.168.209.133/myproject/myapp:latest

有了这段配置,kubelet在节点上拉私有镜像时就会携带认证信息。生产环境推荐用Harbor的Robot账号而不是admin,权限隔离更干净。Robot账号的密码是Harbor生成的一长串随机字符,粘贴的时候容易漏字符,建议创建后直接复制到secret里,不要手动敲。Robot账号的过期时间也值得注意,默认30天,如果你定期重建集群,最好把过期时间设长一点,避免跑到一半发现secret拉镜像鉴权失败。

4.3 推送镜像的常用方式

本地开发时,把镜像打上Harbor地址的tag,然后推送。

docker tag myapp:latest 192.168.209.133/myproject/myapp:latest docker login 192.168.209.133 docker push 192.168.209.133/myproject/myapp:latest

这一套操作如果能顺利通过,整个链路基本就通了。很多人在这个环节遇到推送失败,报错集中在TLS握手、连接拒绝、证书不受信任这几类,下一节专门聊。另外,如果没有docker环境,可以用skopeo做镜像推送,它不依赖docker daemon,命令类似:

skopeo copy docker-daemon:myapp:latest docker://192.168.209.133/myproject/myapp:latest --dest-creds robot$project:xxx

在Harbor上给项目做副本或迁移时,skopeo也比较好使。

5. 常见问题与排查实录

5.1 推送失败:dial tcp connection refused

报错示例:

Get "https://192.168.209.133/v2/": dial tcp 192.168.209.133:443: connect: connection refused

看到connection refused,先确认端口是否真的在监听。

ss -tlnp | grep 443 docker ps | grep harbor

如果容器没起来,去Harbor目录看日志,常见原因是端口被占用、数据目录权限不对、docker compose版本过低。如果容器起来了但443没监听,检查harbor.yml里的https段落是否配置正确,证书文件路径是否写错。此外,防火墙也别漏掉:ufw或firewalld如果没有放行443,容器端口照样不通。内网环境里防火墙是最容易被忽略的凶手。

这里我再强调一个经验:排查时先用curl http://192.168.209.133:80/v2/试一下,再用https试一下,两个结果能帮你很快区分是HTTP层问题还是TLS层问题。我见过有人对着443端口排查了半天,结果harbor.yml里https段落被注释掉了,Web界面走的是80端口。

5.2 证书不受信任:unknown authority

报错示例:

x509: certificate signed by unknown authority

客户端没有信任Harbor的CA,或者证书里的SAN不包含所访问的IP。对策有两个:一是把ca.crt分发到所有节点的正确目录(见4.1);二是重新生成证书,把访问地址的IP和域名全部写进subjectAltName。我遇到最多的情况是hostname填了域名,客户端访问时却用IP,SAN里又只有域名。调试时可以这样验证证书内容:

openssl s_client -connect 192.168.209.133:443 -showcerts < /dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

如果你看到证书的SAN里只有DNS:harbor.local,而你curl的时候用的IP,那报错就躲不掉。重新生成证书时,把两个都写进SAN,别省这一步。另外,证书的有效期也要看一眼,自签证书有效期设个10年,省得到时候又得重新分发一轮。

5.3 HTTP/HTTPS协议错配

docker和containerd默认都会用HTTPS去连registry,如果Harbor只开放了HTTP,你会看到:

Error response from daemon: Get "http://192.168.209.133/v2/": http: server gave HTTP response to HTTPS client

简单粗暴的做法是把Harbor的https关掉,然后在docker/containerd里把这个地址加入insecure-registries白名单。但我不推荐这种方案,尤其是生产或交付场景。正确的做法就是让Harbor跑HTTPS,客户端信任CA证书,这样所有节点行为一致,不会出现某台机器拉得到、另一台拉不到的问题。

需要说明的是,insecure-registries并非不能用,单机开发环境图省事可以接受。但一旦这个环境要交付给客户或接入CI/CD流程,各种诡异的协议错配会让你疲于奔命。与其到时逐个节点改配置,不如一开始就把HTTPS链路铺好。

5.4 kubeadm初始化的几个高频坑

  • kubelet启动失败但日志不明显:先查cgroup驱动是否一致,再查swap是否真的全关了。
  • master Ready但Pod一直ContainerCreating:CNI没装,或者sandbox_image拉不下来。
  • worker节点join后状态NotReady:证书或网络插件配置没同步,逐个节点检查/etc/kubernetes和CNI配置。
  • 版本不一致导致apiserver和kubelet通信异常:apt安装时明确指定版本,别用默认最新。

我把这些坑整理成一张速查表:

现象大概率原因解决方向
kubelet CrashLoopBackOffcgroup driver不一致containerd设SystemdCgroup=true
Pod ImagePullBackOff私有仓库未信任/无凭证分发CA、配置imagePullSecrets
Harbor容器频繁重启数据目录权限或端口冲突检查/data权限和80/443占用
curl系统info失败Harbor服务未完全启动docker compose ps看状态和日志

这类问题定位多了以后,你会慢慢形成条件反射:先看端口,再看证书,最后才怀疑业务。排查顺序比排查命令本身更重要。很多新手一上来就看日志,结果日志里只有结果没有原因,白白浪费时间。先确认链路通不通,再深入组件内部,效率会高很多。

收尾:一点实操心法

脚本写多了以后,我的体会是:部署脚本最重要的不是炫技,而是把环境差异和人为失误压缩到最小。所以每个脚本开头都做前置检测,能早退就早退;每个关键步骤都打印明确日志;每个配置项都写注释,标注为什么是这个值。把“这次为什么这么配”写进脚本注释里,半年后你回来看代码,会感谢当时的自己。

还有一个小技巧:所有脚本和证书文件都固定放到同一目录,比如/opt/deploy,这样不管是本机复现还是换人接手,路径都是确定的,排查问题的范围会小很多。另外,每次部署完把验证步骤也写成一个脚本,比如检查端口、证书、节点状态、镜像拉取,换环境时一键跑一遍,比自己逐条敲命令踏实得多。一套脚本跑通一次不难,难的是换三台新机器还能一条命令跑通。在反复被现场环境教育过之后,我认为值得在这上面多花时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询