最近在学云原生,想着光看文档不行,得动手搭个真实的 CI/CD 流水线才算入门。折腾了差不多一周,终于把整套流程跑通了。
我的实验环境
手上有 4 台虚拟机:
| 主机名 | IP | 用途 |
|---|---|---|
| k8s-master | 192.168.100.10 | K8s 主节点 |
| k8s-node1 | 192.168.100.11 | Work 节点 |
| k8s-node2 | 192.168.100.12 | Work 节点 |
| harbor | 192.168.100.14 | 私有镜像仓库 |
K8s 集群之前就搭好了,这次主要记录怎么在上面跑 Jenkins、Gitea、ArgoCD 这些东西。
先解决存储问题
一开始我直接装 Jenkins,结果重启 Pod 后所有配置都没了。才意识到得先搞个持久化存储,不然数据全在内存里,重启就完蛋。
在 Harbor 节点上安装 NFS 服务端
yum install -y nfs-utils rpcbindsystemctl enable rpcbind nfs-serversystemctl start rpcbind nfs-server
mkdir -p /data/nfs-sharechmod 777 /data/nfs-share # 简单粗暴给权限,防止 K8s 挂载时 Permission Denied
cat <<EOF >> /etc/exports/data/nfs-share 192.168.100.0/24(rw,sync,no_root_squash,no_all_squash)EOF
exportfs -rshowmount -e localhost # 检查是否生效在 K8s 节点上装客户端
每个 Node 都得装 NFS 客户端,不然挂载不上:
yum install -y nfs-utils
# 测试一下能不能正常挂载mkdir -p /tmp/test-mountmount -t nfs 192.168.100.14:/data/nfs-share /tmp/test-mounttouch /tmp/test-mount/hello.txtumount /tmp/test-mount能创建文件就说明通了。
让 K8s 自动创建 PV
手动创建 PV 太麻烦了,装个 NFS Provisioner 让它自动搞:
# 先装 Helmcurl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# 添加仓库helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
# 安装 NFS Provisionerhelm install nfs-client nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \ --set nfs.server=192.168.100.14 \ --set nfs.path=/data/nfs-share \ --set storageClass.name=nfs-client \ --set storageClass.defaultClass=true \ --namespace kube-system装好后看看有没有 StorageClass:
kubectl get sc有 nfs-client (default) 就对了。
试试能不能自动分配存储:
cat <<EOF > test-pvc.yamlkind: PersistentVolumeClaimapiVersion: v1metadata: name: test-claimspec: accessModes: - ReadWriteMany resources: requests: storage: 1MiEOF
kubectl apply -f test-pvc.yamlkubectl get pvcSTATUS 变成 Bound 就说明能用了。
kubectl delete -f test-pvc.yaml # 测试完删掉开始装 Jenkins
Jenkins 是流水线的大脑,这东西配置项特别多,用 Helm 装省事。
写配置文件
cat <<EOF > jenkins-manual.yamlcontroller: image: repository: "jenkins/jenkins" tag: "lts-jdk17" pullPolicy: "IfNotPresent"
# 资源给足一点,不然跑起来会卡 resources: requests: cpu: "500m" memory: "1024Mi" limits: cpu: "1500m" memory: "2560Mi"
# 管理员账号 admin: createSecret: true username: "admin" password: "admin123"
# NodePort 暴露服务 serviceType: NodePort servicePort: 8080 nodePort: 30080
# 持久化存储 persistence: enabled: true size: 8Gi storageClass: "nfs-client" accessMode: ReadWriteOnce
# 先不装插件,启动后再手动装 installPlugins: []
# 跳过初始向导 javaOpts: "-Xms1024m -Xmx2048m -Djenkins.install.runSetupWizard=false"
serviceAccount: create: true name: jenkinsrbac: create: true readSecrets: trueEOF开始装
kubectl create namespace jenkins
helm repo add jenkins https://charts.jenkins.iohelm repo update
helm install jenkins jenkins/jenkins -n jenkins -f jenkins-manual.yaml
# 等 Pod Runningkubectl get pods -n jenkins -w第一个坑:Service 配置
一开始我参考网上找的配置文件,Service 部分是这么写的:
service: type: NodePort nodePort: 30080结果装完发现 Service 是 ClusterIP,根本没暴露端口,外面访问不了。
翻了下官方文档才知道,新版本改了配置结构:
serviceType: NodePortservicePort: 8080nodePort: 30080改完配置重新部署:
helm upgrade jenkins jenkins/jenkins -n jenkins -f jenkins-manual.yaml然后浏览器访问 http://192.168.100.10:30080,账号 admin/admin123。
装插件
进去后第一件事就是装插件:
- Kubernetes - 让 Jenkins 能在 K8s 里动态生成 Agent
- Pipeline - 构建流水线必须的
- Git - 拉取代码
- Blue Ocean - 好看的流水线 UI(可选)
- Localization: Chinese (Simplified) - 简体中文包(可选)
小坑:官方源下载巨慢,好几个插件报错装不上。后来换了清华源才搞定:
https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json换完源重启 Jenkins 就好了。
让 Jenkins 跑在 K8s 上
这是关键配置,让 Jenkins 能在 K8s 里动态起 Pod 来构建:
-
进入 Manage Jenkins → Clouds → New Cloud
-
选择 Kubernetes,输入名称
kubernetes,点击 Create -
填入以下配置:
- Kubernetes URL:
https://kubernetes.default - Kubernetes Namespace:
jenkins - Credentials: 选择 None(因为我们开启了 RBAC,会自动用 ServiceAccount 认证)
- Jenkins URL:
http://jenkins.jenkins.svc.cluster.local:8080 - Jenkins tunnel:
jenkins.jenkins.svc.cluster.local:50000
- Kubernetes URL:
-
点 Test Connection,看到
Connected to Kubernetes v1.xx就成了
测试一下
写个简单的 Pipeline 试试能不能在 K8s 里跑:
pipeline { agent { kubernetes { yaml ''' apiVersion: v1 kind: Pod spec: containers: - name: shell image: busybox command: ['sleep', 'infinity'] ''' } } stages { stage('Test') { steps { container('shell') { sh 'echo "Success!"' } } } }}跑通了就说明 Jenkins 能在 K8s 里动态创建 Pod 了。
装个 Git 仓库
Gitea 比 GitLab 轻量多了,我这小集群跑 GitLab 估计直接挂。
配置文件
cat <<EOF > gitea-lite.yamlglobal: storageClass: "nfs-client"
# 禁用各种复杂的外部依赖redis: enabled: falseredis-cluster: enabled: falsevalkey: enabled: falsevalkey-cluster: enabled: falsepostgresql: enabled: falsepostgresql-ha: enabled: false
# Gitea 核心配置gitea: # 使用内置 SQLite3 数据库 config: database: DB_TYPE: sqlite3 server: DOMAIN: "gitea.local" ROOT_URL: "http://192.168.100.10:30030/" SSH_DOMAIN: "192.168.100.10" SSH_PORT: "30022"
# 管理员账号 admin: username: "admin" password: "admin123" email: "admin@example.com"
# 网络暴露service: http: type: NodePort port: 3000 nodePort: 30030 ssh: type: NodePort port: 22 nodePort: 30022
# 持久化存储persistence: enabled: true size: 5Gi accessModes: - ReadWriteOnceEOF第二个大坑
一开始我没注意配置文件,直接 helm install,结果它给我拉了一堆 Redis Cluster、PostgreSQL HA 什么的,十几个 Pod 瞬间把集群撑爆了。
赶紧把那些缓存和高可用组件全禁用掉,只留一个 Gitea Pod,世界清净了。
开始装
helm repo add gitea-charts https://dl.gitea.com/charts/helm repo update
kubectl create ns giteahelm install gitea gitea-charts/gitea -n gitea -f gitea-lite.yaml
kubectl get pods -n gitea -w访问 http://192.168.100.10:30030,第一次要点”安装”,数据库选 SQLite3(最简单),其他不用改,直接装。
装完用 admin/admin123 登录,建个测试仓库 demo-app。
Harbor 准备工作
Harbor 我之前就搭好了,在 192.168.100.14 上,域名 reg.westos.org,账号 admin/12345。
让 Jenkins 能推镜像
Jenkins 要往 Harbor 推镜像,得先给它个凭证:
kubectl create secret docker-registry harbor-auth \ --docker-server=reg.westos.org \ --docker-username=admin \ --docker-password=12345 \ --docker-email=admin@westos.org \ -n jenkins提前推个基础镜像
docker pull nginx:latestdocker login reg.westos.org -u admin -p 12345docker tag nginx:latest reg.westos.org/library/nginx:v1docker push reg.westos.org/library/nginx:v1写个简单的 Dockerfile
在 demo-app 仓库根目录建个 Dockerfile:
FROM reg.westos.org/library/nginx:v1RUN echo "Hello from Westos Harbor - v1" > /usr/share/nginx/html/index.html又踩坑了:最开始我用 Alpine + cat 输出内容,结果容器一跑就 CrashLoopBackOff。后来才想明白,cat 执行完进程就退了,容器当然挂。换成 Nginx 才正常。
让 Jenkins 能拉代码
Gitea 仓库是私有的,得给 Jenkins 配个凭证:
- 进入 Manage Jenkins → Credentials → System → Global credentials
- 点击 Add Credentials:
- Kind: Username with password
- Username:
admin - Password:
admin123 - ID:
gitea-auth(记住这个 ID,流水线要用) - Description: Gitea Admin
写第一条流水线
终于到关键环节了,写个 Pipeline 让 Jenkins 自动构建镜像。
在 Jenkins 创建一个新的流水线任务,粘贴以下代码:
pipeline { agent { kubernetes { yaml ''' apiVersion: v1 kind: Pod spec: # 让 Pod 知道域名的 IP hostAliases: - ip: "192.168.100.14" hostnames: - "reg.westos.org"
containers: - name: kaniko # 使用 debug 版本,包含 shell 方便调试 image: gcr.io/kaniko-project/executor:debug command: - sleep - infinity volumeMounts: - name: registry-creds mountPath: /kaniko/.docker/
volumes: - name: registry-creds secret: secretName: harbor-auth items: - key: .dockerconfigjson path: config.json ''' } }
stages { stage('Checkout Code') { steps { git credentialsId: 'gitea-auth', url: 'http://192.168.100.10:30030/admin/demo-app.git', branch: 'main' } }
stage('Build & Push') { steps { container('kaniko') { sh ''' /kaniko/executor \ --context `pwd` \ --dockerfile `pwd`/Dockerfile \ --destination reg.westos.org/library/demo-app:v1 \ --skip-tls-verify \ --insecure ''' } } } }}点”立即构建”,然后就看着 Jenkins 自己跑:
- 在 K8s 里起个 Kaniko Pod
- 从 Gitea 拉代码
- 构建镜像
- 推到 Harbor
成功后去 Harbor 看看,library/demo-app:v1 应该就在那了。
装 ArgoCD 实现 GitOps
ArgoCD 是 GitOps 的灵魂,它会盯着 Git 仓库,一有变化就自动同步到 K8s。
装上去
kubectl create namespace argocdkubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 暴露服务kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 8080, "nodePort": 30090}]}}'
# 获取初始密码kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo浏览器访问 https://192.168.100.10:30090(会报不安全,无视它继续就行),用 admin 和刚才的密码登录。
准备部署配置
在 Gitea 的 demo-app 仓库里建个 deploy 文件夹,写个 deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata: name: demo-app namespace: defaultspec: replicas: 1 selector: matchLabels: app: demo-app template: metadata: labels: app: demo-app spec: # 让 K8s 节点知道怎么解析 reg.westos.org hostAliases: - ip: "192.168.100.14" hostnames: - "reg.westos.org"
containers: - name: demo-app image: reg.westos.org/library/demo-app:v1 imagePullPolicy: Always ports: - containerPort: 80
# 拉取私有镜像的凭证 imagePullSecrets: - name: harbor-auth---apiVersion: v1kind: Servicemetadata: name: demo-app-svc namespace: defaultspec: type: NodePort selector: app: demo-app ports: - port: 80 targetPort: 80 nodePort: 30088注意:要部署到 default 命名空间,那里也得有 Harbor 凭证:
kubectl create secret docker-registry harbor-auth \ --docker-server=reg.westos.org \ --docker-username=admin \ --docker-password=12345 \ --docker-email=admin@westos.org \ -n default我就是忘了这茬,Pod 一直 ImagePullBackOff,查了半天才反应过来。
在 ArgoCD 里配置应用
- 登录 ArgoCD UI
- 点击左上角 + NEW APP
- 填入以下配置:
- Application Name:
demo-app - Project Name:
default - Sync Policy:
Automatic(自动同步)- 勾选 Prune Resources(Git 删了文件,K8s 也跟着删)
- 勾选 Self Heal(手动改 K8s 会被自动还原)
- Repository URL:
http://192.168.100.10:30030/admin/demo-app.git - Revision:
main - Path:
deploy - Cluster URL:
https://kubernetes.default.svc - Namespace:
default
- Application Name:
- 点击 CREATE
创建后,ArgoCD 自己会从 Gitea 拉 YAML 文件部署到 K8s。等一会状态变成 Healthy 和 Synced 就好了。
完整跑一遍
现在 CI/CD 已经通了,测试下完整流程。
CI 部分
- 修改 Gitea 中的
Dockerfile,把v1改成v2:
FROM reg.westos.org/library/nginx:v1RUN echo "Hello from Westos Harbor - v2" > /usr/share/nginx/html/index.html- 在 Jenkins 手动触发构建(或者配置 Webhook 自动触发)
- 修改 Pipeline 中的镜像 tag,把
destination改成demo-app:v2 - 构建成功后,Harbor 里会出现
v2标签的镜像
CD 部分
- 在 Gitea 中进入
deploy目录,编辑deployment.yaml - 把镜像 tag 从
v1改成v2:
image: reg.westos.org/library/demo-app:v2- 提交更改
- 去 ArgoCD 界面,点击
demo-app应用的 Refresh 按钮(如果配置了自动同步,等几十秒它自己会检测到变化) - ArgoCD 会发现新版本镜像,触发滚动更新
- 旧的 Pod 会被 Terminating,新的 Pod 会启动

验证
浏览器打开 http://192.168.100.10:30088,看到 “Hello from Westos Harbor - v2” 就说明整条链路通了!
折腾完的一些感受
搞了这么久,总算把 CI/CD 这套东西跑通了。几个体会:
- 存储别省 - 一开始嫌 NFS 麻烦,后来发现不搞持久化根本没法玩
- 资源给足 - Jenkins 挺吃资源的,配少了会卡死
- GitOps 确实爽 - 改个 YAML 推上去,自动就部署了,再也不用 SSH 上去手动重启容器
- 踩坑是常态 - Service 配置、凭证命名空间、容器生命周期,每个都踩了一遍
如果你也在折腾这些东西,有问题欢迎交流。反正我这一路踩的坑够多了,说不定能帮上忙。