一、前言
在云原生 DevSecOps 实践中,私有镜像仓库是实现容器镜像安全管控、版本管理与自动化分发的核心基础设施。
Harbor 作为企业级开源镜像仓库,不仅提供基础的镜像存储功能,更集成了漏洞扫描、签名验证、RBAC 权限控制、镜像复制等安全特性,能够为 CI/CD 流水线提供从构建到部署的全链路镜像安全保障。
基于 Kubernetes 集群环境,通过 Helm 工具完成 Harbor 的离线部署,全程规避网络依赖问题,同时确保部署方案与后续 DevSecOps 流程无缝兼容,为后续容器化应用开发与安全运维提供稳定、安全的镜像管理能力。
二、环境准备
1.基础环境信息
2.代理配置:让 Harbor 自动同步 Docker Hub 镜像
这是干嘛用的?
你平时拉取官方镜像比如 nginx、mysql、ubuntu,都是从 Docker Hub 拉的。但国内网络慢、不稳定。
我们要做的:让 Harbor 代替你去 Docker Hub 拉镜像,自动缓存、自动同步,以后你集群直接从本地 Harbor 拉,速度飞快、稳定、安全。
3.特殊镜像处理:quay.io/ghcr.io 镜像
这是干嘛用的?
有些镜像 不在 Docker Hub,比如:
quay.io(CoreOS、Prometheus、Cilium 等)
ghcr.io(GitHub 容器仓库)
国内直接拉取失败,必须特殊处理。
总的来说:用能上外网的电脑先下载 -> 传到本地 Harbor -> 集群再从 Harbor 拉取
三、拓扑图

考虑到本次实验部署的业务规模较小、服务数量不多,为简化运维复杂度、降低配置成本,采用非隔离式仓库架构:整体只规划配置一个代码仓库,将业务应用代码、配置文件、部署脚本等全部统一存放至同一仓库中进行管理。
在正式生产环境中,为保障代码安全、权限隔离、版本可控以及故障影响范围最小化,建议对业务代码、配置代码、运维脚本进行分仓库物理隔离部署,实现权限分级管理与资源独立维护
四、安装和配置Harbor
在内网部署CI/CD流水线时候,镜像拉取就是一个老生常谈的问题。由于内网环境与公网隔离,常规的镜像拉取方式失效,依靠手动操作又过于低效。所以预先搭建Harbor,作为统一内网镜像托管中心,以彻底解决内网环境下的镜像存储与分发问题。
使用Helm方法搭建的Harbor,但是依然需要手动上传Harbor镜像包,可以去官网下载Harbor离线包,内有完整镜像。
此外,需要注意的是K8s 1.24以上的版本正式放弃Docker,采用的是containerd作为容器运行时,意味着镜像的可见性和存储都是containerd接管而不是传统Docker镜像库。所以镜像要导入进containerd特定空间,才能让K8s集群正常识别并调用镜像
由于 Ubuntu 默认并未内置 Helm,需要手动进行安装。根据网络环境的不同,通常有两种方案:一种是在线安装:适用于有代理的环境,另一种则是离线安装:通过下载二进制文件手动部署。将写入两种场景进行安装
1.安装helm
这一步主要是为了具备通过Helm 包管理工具在 K8s 集群上快速部署 Harbor 仓库的前提条件
执行命令
#相信自己的机器有代理
#临时代理,只在当前shell进行代理
export https_proxy=http://<代理IP:端口>
export http_proxy=http://<代理IP:端口>
export all_proxy=socks5://<代理IP:端口>
#系统永久代理,每次启动都有代理
sudo nano /etc/environment
#写入代理
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
http_proxy="http://<代理IP:端口>"
https_proxy="http://<代理IP:端口>"
ftp_proxy="http://<代理IP:端口>"
no_proxy="localhost,127.0.0.1,172.16.11.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12"
#方法一:一键使用snap来安装helm
sudo snap install helm --classic
#方法二:官方自动化脚本安装
sudo curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4
#加权执行
sudo ./get_helm.sh
#方法三:离线安装
#下载官方二进制包(.tar.gz)https://github.com/helm/helm/releases
#解压
tar -zxvf helm-v4.x.x-linux-amd64.tar.gz
#移动并赋予执行权限
sudo mv linux-amd64/helm /usr/local/bin/helm && sudo chmod +x /usr/local/bin/helm
#验证
helm version执行结果

2.安装Harbor
Helm 安装方式具备自动化、一键化、离线可用的特点,完美适配小型实验环境;代理配置实现了外网镜像同步加速,内部白名单保证集群通信稳定,整体部署结构简洁、易于理解,可作为 DevSecOps 体系中的镜像仓库基础平台。
执行命令
# 1. 创建持久化挂载
#在worker-02*(pv.yaml定义挂载节点)创建文件
sudo mkdir -p /mnt/harbor/{registry,database,redis,chartmuseum,jobservice,trivy}
#给权限
sudo chmod -R 777 /mnt/harbor
# 2. 在 master 运行这个 yaml 文件, 配置文件在下面
kubectl apply -f harbor.pv.yaml
#验证
kubectl get pv
# 3. 投递Harbor离线安装包进虚拟机
#解压安装包 master节点不用做
tar -zxvf harbor-offline-installer-v2.14.3.tgz
#将解压出的镜像包导入 containerd 存储
cd harbor && sudo ctr -n k8s.io images import harbor.v2.14.3.tar.gz
# 4. 在 master 上使用 Helm 部署 Harbor 私有镜像仓库
helm install harbor harbor/harbor \
# 安装 Harbor,将 Helm 发布实例命名为 harbor,便于管理
--namespace harbor --create-namespace \
# 指定命名空间为 harbor,不存在则自动创建,实现资源隔离
--set expose.type=nodePort \
# 使用 NodePort 方式暴露服务,支持通过节点 IP + 端口访问
--set expose.tls.enabled=false \
# 关闭 HTTPS 加密(仅测试环境使用,生产必须开启 TLS)
--set externalURL=http://172.16.11.53:30002 \
# 外部访问地址,用于 docker pull、镜像推送、控制台登录
--set image.tag=v2.14.3 \
# 指定使用的镜像版本,与离线安装包保持一致
--set image.pullPolicy=IfNotPresent \
# 镜像拉取策略:本地有则使用本地镜像,避免联网下载
--set persistence.enabled=false \
# 临时关闭持久化存储(仅实验环境)
# 警告:Pod 重建、节点重启、删除命名空间都会导致数据丢失
# 生产环境必须启用持久化存储(PVC)
--set proxy.httpProxy="http://10.10.1.78:7890" \
# 配置 HTTP 代理,用于 Harbor 拉取外网镜像(Docker Hub)
--set proxy.httpsProxy="http://10.10.1.78:7890" \
# 配置 HTTPS 代理
--set proxy.noProxy="127.0.0.1,localhost,.local,.internal,harbor-core,harbor-jobservice,harbor-database,harbor-registry,harbor-portal,10.96.0.0/12,10.244.0.0/16,172.16.0.0/12"
# 代理白名单(不走代理)
# 包含本地地址、内部服务、集群网段、Harbor 自身组件
# 目的:内部通信不走外网代理,保证速度与稳定性
# 4. 验证安装
helm get values harbor -n harborharbor.pv.yaml文件
这个文件一共创建了 6 个持久化存储(PV),分别给 Harbor 的 6 个组件用:
harbor-registry-pv → 存镜像文件(最重要)
harbor-database-pv → 存数据库(用户、项目、权限)
harbor-redis-pv → 存缓存
harbor-chartmuseum-pv → 存 Helm Chart
harbor-jobservice-pv → 存任务日志
harbor-trivy-pv → 存漏洞扫描库
# 1.Harbor镜像仓库存储(Registry)
apiVersion: v1
kind: PersistentVolume
metadata:
name: harbor-registry-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: harbor-registryl-storage
hostPath:
path: /mnt/harbor/registry
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-02
---
# 2.Harbor数据库存储(Database)
apiVersion: v1
kind: PersistentVolume
metadata:
name: harbor-database-pv
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: harbor-database-storage
hostPath:
path: /mnt/harbor/database
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-02
---
# 3.Harbor缓存存储(Redis)
apiVersion: v1
kind: PersistentVolume
metadata:
name: harbor-redis-pv
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: harbor-redis-storage
hostPath:
path: /mnt/harbor/redis
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-02
---
# 4.Harbor Chartmuseum存储
apiVersion: v1
kind: PersistentVolume
metadata:
name: harbor-chartmuseum-pv
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: harbor-chartmuseum-storage
hostPath:
path: /mnt/harbor/chartmuseum
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-02
---
# 5.Harbor Jobservice
apiVersion: v1
kind: PersistentVolume
metadata:
name: harbor-jobservice-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: harbor-jobservice-storage
hostPath:
path: /mnt/harbor/jobservice
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-02
---
# 6. Harbor 漏洞扫描存储 (Trivy)
apiVersion: v1
kind: PersistentVolume
metadata:
name: harbor-trivy-pv
spec:
capacity:
storage: 5Gi # 漏洞库比较大,建议至少 5Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: harbor-trivy-storage
hostPath:
path: /mnt/harbor/trivy
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-02验证安装结果

访问 Harbor 控制台

2.Harbor代理
在内网部署 CI/CD 流程中,Harbor不仅仅是发挥镜像托管与中转作用,还承担着对接上游镜像仓库,实现自动拉取公网镜像并进行本地缓存。并且在安装Harbor的时候,已经明确写入代理,就不用去修改congfigmap文件。确保了环境的一致性和可维护性
创建Harbor代理镜像过程视频如下,有点粗糙和显示不全请见谅,建议重点关注配置流程,这也只是对接Docker hub上的,也可以去对接quay.io和 ghcr.io,因为有些镜像Docker hub上没有
3.配置不安全仓库(HTTP)
在内网环境下,Harbor运行的是HTTP模式,但是K8s因为安全策略默认走的是HTTPS,所以需要配置不安全仓库,实现内部通信协议降级,让集群内部信任不安全仓库,拉取未加密的私有镜像源
执行命令
# 1. Master 节点配置(Docker 信任 Harbor)
sudo nano /etc/docker/daemon.json
#写入以下固定内容
{
"insecure-registries": ["172.17.11.104:30002"]
}
# 重启服务生效
sudo systemctl daemon-reload
sudo systemctl restart docker
# 2. 所有 Worker 节点配置(containerd 信任 Harbor)
sudo mkdir -p /etc/containerd/certs.d/172.17.11.104:30002
# 写入配置文件
sudo tee /etc/containerd/certs.d/172.17.11.104:30002/hosts.toml <<EOF
server = "http://172.17.11.104:30002"
[host."http://172.17.11.104:30002"]
capabilities = ["pull", "resolve"]
skip_verify = true
EOF
# 检查 containerd 配置
sudo nano /etc/containerd/config.toml
# 重启生效
sudo systemctl restart containerd
# 3. 拉取镜像
sudo crictl pull 172.17.11.104:30002/proxy/redis:latest
# 验证是否成功拉取镜像
sudo crictl images | grep redissudo nano /etc/containerd/config.toml这一步是验证 K8s 节点能从 Harbor 拉取镜像,确认 containerd 配置、Harbor 代理和网络链路都正常,提前排雷,避免后续部署 Pod 时出现拉取失败的问题执行结果

4.安装和配置Argo CD
我们在配置Argo CD我们首先要知道它是来做什么的
我们可以把 Argo CD 理解成 K8s 的 “全自动运维管家”,核心就是帮我们管应用的部署和更新,让整个过程又稳又省心。
它的核心逻辑:GitOps 到底是什么?
简单说就是 “用 Git 当唯一的‘配置源’,所有操作都从 Git 来”。
你把应用的配置(比如 Pod 怎么跑、用哪个镜像、开多少个副本)写进 Git 仓库
Argo CD 会自动盯着这个 Git 仓库
一旦你改了 Git 里的配置,Argo CD 就会自动把 K8s 里的应用更新成和 Git 里一模一样的状态
举个例子:你想把应用的镜像从 v1 升级到 v2,不用手动去 K8s 里敲命令,只要在 Git 里把镜像版本改成 v2,提交代码,Argo CD 就会自动帮你完成更新,全程不用人工干预。
它解决了什么问题?
避免 “配置漂移”:以前可能有人手动改 K8s 配置,改了没记录,时间长了没人知道现在的配置是啥样,Argo CD 让所有配置都在 Git 里,清清楚楚,谁改了什么、什么时候改的都有记录
部署更稳定:所有操作都是自动化的,不用怕人工敲错命令导致部署失败
方便回滚:如果更新出问题,直接在 Git 里回退到上一个版本,Argo CD 就会自动把应用也回退回去,不用再手动折腾
执行命令
# 第一个方法:有网环境安装 Argo CD
# 创建命名空间
kubectl create namespace argocd
# 一键安装 Argo CD(自动拉镜像、自动部署)
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 第二个方法:无网环境安装 Argo CD(最常用、企业内网必学)
# 无网 = 不能访问公网,必须把镜像先放进 Harbor
# 步骤 1:在能上网的机器下载配置文件
curl -L https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml -o argocd-install.yaml
#步骤 2:下载 Argo CD 必需镜像
docker pull quay.io/argoproj/argocd:v2.13.3
docker pull ghcr.io/dexidp/dex:v2.41.1
docker pull redis
# 步骤 3:打包镜像
docker save quay.io/argoproj/argocd:v2.13.3 ghcr.io/dexidp/dex:v2.41.1 redis -o argocd.tar
# 步骤 4:把 argocd.tar 传到内网 master 节点
# 在内网 master 执行
sudo docker load < argocd.tar
# 步骤 5:打标签并推送到你的 Harbor
sudo docker tag quay.io/argoproj/argocd:v2.13.3 172.16.11.104:30002/library/argocd:v2.13.3
sudo docker tag ghcr.io/dexidp/dex:v2.41.1 172.16.11.104:30002/library/dex:v2.41.1
sudo docker tag redis 172.16.11.104:30002/proxy/redis
sudo docker push 172.16.11.104:30002/library/argocd:v2.13.3
sudo docker push 172.16.11.104:30002/library/dex:v2.41.1
sudo docker push 172.16.11.104:30002/proxy/redis
# 步骤 6:修改 YAML,把镜像全部指向内网 Harbor
sed -i 's|quay.io/argoproj|172.16.11.104:30002/library|g' argocd-install.yaml
sed -i 's|image: redis|image: 172.16.11.104:30002/proxy/redis|g' argocd-install.yaml
sed -i 's|library/dex:.*|172.16.11.104:30002/library/dex:v2.41.1|g' argocd-install.yaml
# 步骤 7:检查有没有漏改的镜像
grep "image:" argocd-install.yaml | grep -v "172.16.11.104"
# 不出东西 = 全部改成功
# 步骤 8:开始安装 Argo CD
kubectl apply -f argocd-install.yaml -n argocd --server-side
# 步骤 9:查看是否启动成功
kubectl get pods -n argocd -w
# 步骤 10:如果报错,查看详细原因
kubectl describe pod 你的pod名字 -n argocd执行结果

如果每个pod全部running,就可以查看初次登录的密码了,记得一定要base64 转码(中间隔断一下,要不然代码太长了 看着累)
#自定义Argo CD端口
#作用:让你可以在浏览器外网 / 内网访问 Argo CD 面板
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 8080, "nodePort": 30000}]}}'
#获取登录密码
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d访问对应页面https://<Master_IP>:30000输入账号:admin

这里的配置先告一段落,服务起来就好,再去创建Gitea(代码仓库),部署好Gitea后和Argo CD去串联,实现业务自动化部署。
五、安装和部署Gitea
部署Gitea就简单多了,镜像也是直接用Harbor做代理下来就行
执行命令
#对应gitea.yaml文件
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
namespace: gitea
labels:
app: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: <对应HarborIP:端口>/<代理项目>/gitea/gitea:latest
imagePullPolicy: IfNotPresent
env:
- name: USER_UID
value: "1000"
- name: USER_GID
value: "1000"
- name: GITEA__webhook__ALLOWED_HOST_LIST
value: "private"
ports:
- containerPort: 3000
name: http
- containerPort: 22
name: ssh
volumeMounts:
- mountPath: /data
name: gitea-data
nodeName: <对应节点名>
volumes:
- name: gitea-data
hostPath:
path: /mnt/gitea/data
---
apiVersion: v1
kind: Service
metadata:
name: gitea-service
namespace: gitea
spec:
ports:
- name: http
port: 80
targetPort: 3000
- name: ssh
port: 2222
targetPort: 22
selector:
app: gitea
# 去woker-02节点去运用
sudo mkdir -p /mnt/gitea/data
# 开放权限 确保容器进程拥有完整的读写权限,仅测试用
sudo chmod -R 777 /mnt/gitea/data
# 以下在master上做
#创建空间
kubectl create ns gitea
#运用yaml
kubectl apply -f gitea.yaml
# 查看状态
kubectl get pods -n gitea -w
kubectl get svc -n gitea
#运行完成在woker-02降级处理
#正常运行后,降级处理
sudo chown -R 1000:1000 /mnt/gitea/data以下是关于 YAML 配置细节的补充说明
环境变量 (Env)上设置了
- name: GITEA__webhook__ALLOWED_HOST_LIST value: "private"解除了Gtiea默认禁用向私有IP发送Webhook请求,为了对接后续部署的Jenkins流水线
存储卷使用了物理挂载,在对应的节点创建目录给予对应权限。若不通过
nodeName进行节点锁定,一旦发生 Pod 自动漂移(重新调度到其他节点),Gitea 将因无法访问原始物理路径而导致镜像库、数据库等核心数据丢失
初始化参考我前面的文章,但是端口可以不用改,无所谓https://shuta.pigeon.show/?p=764
六、安装NPM(Nginx Proxy Manager )
其实这个组件并非强制项,但有了它,域名看着确实顺眼不少(主要目的),统一化管理(不是)。配置NPM很快,通过接入 Cloudflare 的泛域名证书,可以实现全站 HTTPS 化,告别 IP+端口的原始感。
以上如同废话,开始部署yaml,诶这时候就可以开始使用Gitea和Argo CD进行串联了
进入Gitea,对应的yaml(其实这是一个悖论,因为我的gitea没有暴露端口出来,所以还是kubectl apply -f)
apiVersion: apps/v1
kind: Deployment
metadata:
name: npm-db
namespace: npm
labels:
app: npm-db
spec:
replicas: 1
selector:
matchLabels:
app: npm-db
template:
metadata:
labels:
app: npm-db
spec:
containers:
- name: db
image: <对应HarborIP:端口>/<代理项目>/library/mariadb:10.4
imagePullPolicy: IfNotPresent
env:
- name: MYSQL_ROOT_PASSWORD
value: npm_root_pwd
- name: MYSQL_DATABASE
value: npm
- name: MYSQL_USER
value: npm_user
- name: MYSQL_PASSWORD
value: npm_pass
volumeMounts:
- mountPath: /var/lib/mysql
name: db-data
nodeName: worker-02
volumes:
- name: db-data
hostPath:
path: /mnt/npm/db
---
apiVersion: v1
kind: Service
metadata:
name: npm-db
namespace: npm
spec:
ports:
- port: 3306
selector:
app: npm-db
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: npm
namespace: npm
labels:
app: npm
spec:
replicas: 1
selector:
matchLabels:
app: npm
template:
metadata:
labels:
app: npm
spec:
initContainers:
- name: wait-for-db
image: <对应HarborIP:端口>/<代理项目>/library/busybox:latest
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'until nc -z npm-db 3306; do echo waiting for db; sleep 2; done;']
containers:
- name: npm
image: <对应HarborIP:端口>/<代理项目>/jc21/nginx-proxy-manager:latest
imagePullPolicy: IfNotPresent
env:
- name: DB_MYSQL_HOST
value: npm-db
- name: DB_MYSQL_PORT
value: "3306"
- name: DB_MYSQL_USER
value: npm_user
- name: DB_MYSQL_PASSWORD
value: npm_pass
- name: DB_MYSQL_NAME
value: npm
ports:
- containerPort: 81
- containerPort: 80
- containerPort: 443
volumeMounts:
- mountPath: /data
name: npm-data
- mountPath: /etc/letsencrypt
name: npm-letsencrypt
nodeName: <你爱存储挂载节点>
volumes:
- name: npm-data
hostPath:
path: /mnt/npm/data
- name: npm-letsencrypt
hostPath:
path: /mnt/npm/letsencrypt
---
apiVersion: v1
kind: Service
metadata:
name: npm-service
namespace: npm
spec:
externalIPs:
- <masterIP>
ports:
- name: admin
port: 81
- name: http
port: 80
- name: https
port: 443
selector:
app: npm
#去对应节点创建目录
mkdir -p /mnt/npm/db /mnt/npm/data /mnt/npm/letsencrypt
chmod -R 777 /mnt/npm/
#后续根据UID细化 但是我好像只是775
# 回到 master 创建命名空间
kubectl create namespace npm
# 部署 NPM
kubectl apply -f npm.yaml -n npm
# 查看是否启动成功
kubectl get pods -n npm -w以下是关于 YAML 配置细节的补充说明
等待db容器,等待是为了避免更多的报错
initContainers:
- name: wait-for-db
image: <对应HarborIP:端口>/<代理项目>/library/busybox:latest
command: ['sh', '-c', 'until nc -z npm-db 3306; do echo waiting for db; sleep 2; done;']这里使用 busybox镜像执行循环脚本,不断去探查npm-db:3306是否存活,防止数据库还没启动完毕,NPM 就抢先启动导致报错崩溃。只有数据库准备好了,主容器才会开始运行
指定流量入口,作为流量网关
spec:
externalIPs:
- <masterIP>配置NPM的Service资源,通过externalIPs指定流量入口。当外部流量访问该 IP时,K8s将根据端口映射规则,将其作为流量网关转发至对应服务,因此访问http://<masterIP>:81就可以进入NPM服务
默认账号:admin@example.com 密码:changeme 登录后强制更新,填入管理员邮箱和密码就ok了
1.获取证书
获取证书大致流程如下,如果申请失败,请看看是否token复制完没有,证书申请要等一会,做NPM反代了应该有自己的域名吧(点头)
2.建立反向代理
已知K8s集群内部内置DNS服务,且Service的域名通常以.svc.cluster.local结尾。推荐使用内部域名而并非Pod_IP进行反向代理。防止出现Pod漂移和重启导致IP不一致,反向代理失败
#查看集群范围内的服务信息
kubectl get svc -A
就以Gitea和Harbor服务举例,在NPM里配置如下
Gitea;配置图形步骤


配置结果使用域名访问:

Harbor:配置步骤图
图一:

图二:

配置结果使用域名访问:

七、安装和配置Jenkins
我们现在的进度是;Gitea(代码库)-> Nginx Proxy Manager(域名代理)-> Jenkins(自动构建、打包镜像、推 Harbor)
1.Jenkins 是干嘛的?
Jenkins 就是 自动化打包工具:
从 Gitea 拉代码
自动把代码做成 Docker 镜像
自动推送到你的 Harbor 仓库
通知 Argo CD 自动更新部署
它是 CI(持续集成)核心服务,必须部署。
2.和 NPM 部署有什么不一样?
和 NPM 几乎一样,只多一个东西:
Jenkins 需要 PV / PVC 做数据持久化
(NPM 你用的是 hostPath,Jenkins 企业环境标准用 PV/PVC 更稳定)
PV:硬盘空间申请PVC:使用硬盘空间
其余流程一模一样:创建目录 -> 授权 -> 部署 YAML -> 访问页面 -> 配置凭证
3.配置步骤
执行命令
步骤一:在woker-02 提前做创建目录和权限提升
sudo mkdir -p /mnt/jenkins_data
sudo chmod 777 /mnt/jenkins_data步骤二:在 mater 上部署 PV:硬盘
#jenkins-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkins-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
hostPath:
path: /mnt/jenkins_data
# 启动yaml文件
kubectl apply -f jenkins.yaml
# 检查
kubectl get pv
#看到 jenkins-pv、状态 Available 就是成功步骤三:在 master 部署 Jenkins 主服务
#去gitea部署jenkins.yaml
apiVersion: v1
kind: Namespace
metadata:
name: jenkins
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins-admin
namespace: jenkins
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jenkins-agent-role
namespace: jenkins
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log", "persistentvolumeclaims", "secrets"]
verbs: ["get", "list", "watch", "create", "delete", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jenkins-agent-binding
namespace: jenkins
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins-agent-role
subjects:
- kind: ServiceAccount
name: jenkins-admin
namespace: jenkins
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-pvc
namespace: jenkins
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins
namespace: jenkins
labels:
app: jenkins
spec:
replicas: 1
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
serviceAccountName: jenkins-admin
nodeSelector:
kubernetes.io/hostname: <挂载节点>
containers:
- name: jenkins
image: '<对应HarborIP:端口>/<代理项目>/jenkins/jenkins:lts'
imagePullPolicy: IfNotPresent
securityContext:
runAsUser: 0
ports:
- containerPort: 8080
name: http
- containerPort: 50000
name: jnlp
resources:
limits:
cpu: '2'
memory: 2Gi
requests:
cpu: '1'
memory: 1Gi
env:
- name: JAVA_OPTS
value: >-
-Xmx1536m -XshowSettings:vm
-Dhudson.slaves.NodeProvisioner.initialDelay=0
-Dhudson.slaves.NodeProvisioner.MARGIN=50
-Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
-Duser.timezone=Asia/Shanghai
-Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Shanghai
volumeMounts:
- mountPath: /var/jenkins_home
name: data
volumes:
- name: data
persistentVolumeClaim:
claimName: jenkins-pvc
---
apiVersion: v1
kind: Service
metadata:
name: jenkins
namespace: jenkins
labels:
app: jenkins
spec:
type: NodePort
selector:
app: jenkins
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 32001
- name: jnlp
port: 50000
targetPort: 50000
nodePort: 32002
# 启动yaml文件
kubectl apply -f jenkins.yaml
# 检查
kubectl get pods -n jenkins -w
#等它变成 Running 即可步骤四:在 master 执行,拿登录密码
kubectl exec -n jenkins $(kubectl get pods -n jenkins | grep Running | awk '{print $1}') -- cat /var/jenkins_home/secrets/initialAdminPassword
# 浏览器访问
http://MasterIP:32001
# 默认用户admin以下是关于 YAML 配置细节的补充说明
动态调度权限
通过
ServiceAccount绑定Role,允许Jenkins Master在集群内自己的命名空间自动创建和销毁构建 Pod。
性能调优
内存优化:将
-Xmx设为容器限制的75%,为非堆内存预留空间,防止因OOM导致 Pod 频繁重启调度优化:让 Jenkins 发现任务排队时立即申请 K8s 资源
时区对齐:确保日志和构建记录显示北京时间,防止出现日记出现时差
持久化挂载
越过权限:配置
runAsUser: 0以 root 运行以确保能读写挂载的磁盘数据持久化:过 PV/PVC 将
/var/jenkins_home挂载到宿主机,确保插件、凭据和 Job 配置在 Pod 重启后不会丢失
这两个文件提交保存成功后,再去Argo CD部署对应的仓库项目,这样才把服务提起来

八、配置Jenkins
等待是最好的摸鱼,选择推荐安装插件,报错无所谓,等会里面去换其他都行

进入初始化界面,点击右上角齿轮 -> 点击插件管理

主要下载这几个插件:Kubernetes、Gitea、Pipeline、Multibranch Pipeline Inline Definition Plugin,如果没安装的去第二个插件市场安装插件




插件下载的时候,请勾选“安装完成后重启 Jenkins”选项。只有在Jenkins成功重启并重新加载插件库后,新安装的插件才能正式启用

九、对接Gitea
要实现Jenkins自动拉取Gitea源码,必须完成双向的身份验证
Gitea 端(获取凭证):进入用户设置,生成 Access Token(访问令牌)
Jenkins 端(生效凭证):将获取的 Token 存入 Jenkins 的 凭据管理并配置对应Gitea Server
GItea创建访问令牌
流程:头像菜单下拉设置→应用→生成新令牌选择对应权限→生成新令牌
能够查看令牌的机会只有这一次,记得先保存好

Jenkins添加Gitea访问令牌

选择Gitea Personal Access Token

Jenkins 中配置 Gitea Server

勾选Manage hook后,后续进行对接的时候Gitea Plugin 插件自动为新建的 Jenkins 项目创建 Webhook
顺便把jenkins解决自己对外身份

构建任务
新建任务,选择这个0


新增一个选择

等待成功扫描

选择多分支流水线

是点对点,选择仓库里面有jenkinsfile文件的仓库,这样也能成功
测试连通性的Jenkinsfile
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'echo build'
}
}
stage('Test'){
steps {
sh 'echo test'
}
}
stage('Deploy') {
steps {
sh 'echo publish'
}
}
}
}验证
进行验证

查看这里结果点击立即构建,成功就是成功,失败查看日志


这里会自动生成一个Webhook
十、镜像更新器
拓扑图里面只有编译/构建新镜像,并没有自动化更新镜像。要实现“镜像更新即自动部署”这个时候需要引入Argo CD Image Updater(镜像更新器)
Argo CD Image Updater会持续性的。一旦发现有符合规则的新版本镜像,它会自动更新Argo CD应用状态,且去直接修改 Git 仓库中的镜像 Tag,从而触发集群的自动滚动更新
如何安装镜像更新器,就可以参考我的GitOps:制品管理与版本策略 – GUGa这篇文章的版本策略
十一、验证流程
视频演示了从代码提交到自动部署的全过程(约 2 分钟)。虽然视频中包含一些杂乱的中间操作,但最终完整展示了CI/CD全链路自动化的实现方案,做到了“代码即部署”
每个文件所用代码
redis.yaml
#redis.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-deployment
spec:
replicas: 2
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: <对应HarborIP:端口>/public/redis:7.0
ports:
- containerPort: 30081kustomization.yaml
#kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- redis.yamlJenkinsfile
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins-admin
containers:
- name: kaniko
image: harbor.pigeon.show/public/executor:debug
command: ['sleep']
args: ['99d']
volumeMounts:
- name: harbor-auth
mountPath: /kaniko/.docker
- name: jnlp
image: harbor.pigeon.show/proxy/jenkins/inbound-agent:latest
volumes:
- name: harbor-auth
secret:
secretName: harbor-secret
items:
- key: .dockerconfigjson
path: config.json
"""
}
}
// 每 2 分钟扫描一次代码
triggers {
pollSCM('H/2 * * * *')
}
stages {
stage('生产镜像') {
// 【核心防死循环逻辑】
// 如果Git提交消息中包含 "automatic update",说明是镜像更新器,直接跳过此阶段
when {
not {
changelog '.*automatic update of.*'
}
}
steps {
// 拉取代码
checkout scm
container('kaniko') {
// 产出 7.x 格式镜像
sh """
/kaniko/executor --context ${WORKSPACE} \
--dockerfile Dockerfile \
--destination harbor.pigeon.show/public/redis:7.${BUILD_NUMBER} \
--skip-tls-verify
"""
}
}
}
}
post {
success {
echo "流程处理完成"
}
aborted {
echo "检测到镜像更新器提交,已跳过构建以防止死循环"
}
}
}Dockerfile
FROM harbor.pigeon.show/proxy/library/redis:7-alpine
RUN echo "build by jenkins ci" > /build_info.txt