Administrator
发布于 2026-05-17 / 12 阅读
0
0

基于K8s部署CI/CD自动化流水实现DevSecOps

一、前言

在云原生 DevSecOps 实践中,私有镜像仓库是实现容器镜像安全管控、版本管理与自动化分发的核心基础设施。

Harbor 作为企业级开源镜像仓库,不仅提供基础的镜像存储功能,更集成了漏洞扫描、签名验证、RBAC 权限控制、镜像复制等安全特性,能够为 CI/CD 流水线提供从构建到部署的全链路镜像安全保障。

基于 Kubernetes 集群环境,通过 Helm 工具完成 Harbor 的离线部署,全程规避网络依赖问题,同时确保部署方案与后续 DevSecOps 流程无缝兼容,为后续容器化应用开发与安全运维提供稳定、安全的镜像管理能力。

二、环境准备

1.基础环境信息

组件

版本 / 配置

说明

Kubernetes 集群

v1.35

单主节点(master-01)+ 多工作节点架构(worker-1、worker-2)

Helm

v4.1.4

Kubernetes 包管理工具,用于 Harbor Chart 部署

Harbor 离线安装包

v2.14.3

含完整镜像文件与 Helm Chart 目录

操作系统

Ubuntu 20.04 LTS

集群节点统一操作系统

网络配置

集群网段:10.244.0.0/16,管理网段:172.16.11.0/24

确保节点间网络互通,代理配置仅用于 Helm 安装阶段

2.代理配置:让 Harbor 自动同步 Docker Hub 镜像

这是干嘛用的?

你平时拉取官方镜像比如 nginxmysqlubuntu,都是从 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 harbor
  • harbor.pv.yaml文件

这个文件一共创建了 6 个持久化存储(PV),分别给 Harbor 的 6 个组件用:

  1. harbor-registry-pv → 存镜像文件(最重要)

  2. harbor-database-pv → 存数据库(用户、项目、权限)

  3. harbor-redis-pv → 存缓存

  4. harbor-chartmuseum-pv → 存 Helm Chart

  5. harbor-jobservice-pv → 存任务日志

  6. 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 redis
  • sudo 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 就是 自动化打包工具

  1. 从 Gitea 拉代码

  2. 自动把代码做成 Docker 镜像

  3. 自动推送到你的 Harbor 仓库

  4. 通知 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

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

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

主要下载这几个插件:KubernetesGiteaPipelineMultibranch 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: 30081

kustomization.yaml

#kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - redis.yaml

Jenkinsfile

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


评论