2456 字
12 分钟
从零搭建 K8s CI/CD 流水线实战记录

最近在学云原生,想着光看文档不行,得动手搭个真实的 CI/CD 流水线才算入门。折腾了差不多一周,终于把整套流程跑通了。

我的实验环境#

手上有 4 台虚拟机:

主机名IP用途
k8s-master192.168.100.10K8s 主节点
k8s-node1192.168.100.11Work 节点
k8s-node2192.168.100.12Work 节点
harbor192.168.100.14私有镜像仓库

K8s 集群之前就搭好了,这次主要记录怎么在上面跑 Jenkins、Gitea、ArgoCD 这些东西。

先解决存储问题#

一开始我直接装 Jenkins,结果重启 Pod 后所有配置都没了。才意识到得先搞个持久化存储,不然数据全在内存里,重启就完蛋。

在 Harbor 节点上安装 NFS 服务端#

Terminal window
yum install -y nfs-utils rpcbind
systemctl enable rpcbind nfs-server
systemctl start rpcbind nfs-server
mkdir -p /data/nfs-share
chmod 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 -r
showmount -e localhost # 检查是否生效

在 K8s 节点上装客户端#

每个 Node 都得装 NFS 客户端,不然挂载不上:

Terminal window
yum install -y nfs-utils
# 测试一下能不能正常挂载
mkdir -p /tmp/test-mount
mount -t nfs 192.168.100.14:/data/nfs-share /tmp/test-mount
touch /tmp/test-mount/hello.txt
umount /tmp/test-mount

能创建文件就说明通了。

让 K8s 自动创建 PV#

手动创建 PV 太麻烦了,装个 NFS Provisioner 让它自动搞:

Terminal window
# 先装 Helm
curl 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 Provisioner
helm 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:

Terminal window
kubectl get sc

nfs-client (default) 就对了。

试试能不能自动分配存储:

cat <<EOF > test-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-claim
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi
EOF
kubectl apply -f test-pvc.yaml
kubectl get pvc

STATUS 变成 Bound 就说明能用了。

Terminal window
kubectl delete -f test-pvc.yaml # 测试完删掉

开始装 Jenkins#

Jenkins 是流水线的大脑,这东西配置项特别多,用 Helm 装省事。

写配置文件#

cat <<EOF > jenkins-manual.yaml
controller:
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: jenkins
rbac:
create: true
readSecrets: true
EOF

开始装#

Terminal window
kubectl create namespace jenkins
helm repo add jenkins https://charts.jenkins.io
helm repo update
helm install jenkins jenkins/jenkins -n jenkins -f jenkins-manual.yaml
# 等 Pod Running
kubectl get pods -n jenkins -w

第一个坑:Service 配置#

一开始我参考网上找的配置文件,Service 部分是这么写的:

service:
type: NodePort
nodePort: 30080

结果装完发现 Service 是 ClusterIP,根本没暴露端口,外面访问不了。

翻了下官方文档才知道,新版本改了配置结构:

serviceType: NodePort
servicePort: 8080
nodePort: 30080

改完配置重新部署:

Terminal window
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 来构建:

  1. 进入 Manage JenkinsCloudsNew Cloud

  2. 选择 Kubernetes,输入名称 kubernetes,点击 Create

  3. 填入以下配置:

    • 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
  4. 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.yaml
global:
storageClass: "nfs-client"
# 禁用各种复杂的外部依赖
redis:
enabled: false
redis-cluster:
enabled: false
valkey:
enabled: false
valkey-cluster:
enabled: false
postgresql:
enabled: false
postgresql-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:
- ReadWriteOnce
EOF

第二个大坑#

一开始我没注意配置文件,直接 helm install,结果它给我拉了一堆 Redis Cluster、PostgreSQL HA 什么的,十几个 Pod 瞬间把集群撑爆了。

赶紧把那些缓存和高可用组件全禁用掉,只留一个 Gitea Pod,世界清净了。

开始装#

Terminal window
helm repo add gitea-charts https://dl.gitea.com/charts/
helm repo update
kubectl create ns gitea
helm 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 推镜像,得先给它个凭证:

Terminal window
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

提前推个基础镜像#

Terminal window
docker pull nginx:latest
docker login reg.westos.org -u admin -p 12345
docker tag nginx:latest reg.westos.org/library/nginx:v1
docker push reg.westos.org/library/nginx:v1

写个简单的 Dockerfile#

demo-app 仓库根目录建个 Dockerfile:

FROM reg.westos.org/library/nginx:v1
RUN echo "Hello from Westos Harbor - v1" > /usr/share/nginx/html/index.html

又踩坑了:最开始我用 Alpine + cat 输出内容,结果容器一跑就 CrashLoopBackOff。后来才想明白,cat 执行完进程就退了,容器当然挂。换成 Nginx 才正常。

让 Jenkins 能拉代码#

Gitea 仓库是私有的,得给 Jenkins 配个凭证:

  1. 进入 Manage JenkinsCredentialsSystemGlobal credentials
  2. 点击 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 自己跑:

  1. 在 K8s 里起个 Kaniko Pod
  2. 从 Gitea 拉代码
  3. 构建镜像
  4. 推到 Harbor

成功后去 Harbor 看看,library/demo-app:v1 应该就在那了。

装 ArgoCD 实现 GitOps#

ArgoCD 是 GitOps 的灵魂,它会盯着 Git 仓库,一有变化就自动同步到 K8s。

装上去#

Terminal window
kubectl create namespace argocd
kubectl 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/v1
kind: Deployment
metadata:
name: demo-app
namespace: default
spec:
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: v1
kind: Service
metadata:
name: demo-app-svc
namespace: default
spec:
type: NodePort
selector:
app: demo-app
ports:
- port: 80
targetPort: 80
nodePort: 30088

注意:要部署到 default 命名空间,那里也得有 Harbor 凭证:

Terminal window
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 里配置应用#

  1. 登录 ArgoCD UI
  2. 点击左上角 + NEW APP
  3. 填入以下配置:
    • 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
  4. 点击 CREATE

创建后,ArgoCD 自己会从 Gitea 拉 YAML 文件部署到 K8s。等一会状态变成 HealthySynced 就好了。

完整跑一遍#

现在 CI/CD 已经通了,测试下完整流程。

CI 部分#

  1. 修改 Gitea 中的 Dockerfile,把 v1 改成 v2
FROM reg.westos.org/library/nginx:v1
RUN echo "Hello from Westos Harbor - v2" > /usr/share/nginx/html/index.html
  1. 在 Jenkins 手动触发构建(或者配置 Webhook 自动触发)
  2. 修改 Pipeline 中的镜像 tag,把 destination 改成 demo-app:v2
  3. 构建成功后,Harbor 里会出现 v2 标签的镜像

CD 部分#

  1. 在 Gitea 中进入 deploy 目录,编辑 deployment.yaml
  2. 把镜像 tag 从 v1 改成 v2
image: reg.westos.org/library/demo-app:v2
  1. 提交更改
  2. 去 ArgoCD 界面,点击 demo-app 应用的 Refresh 按钮(如果配置了自动同步,等几十秒它自己会检测到变化)
  3. ArgoCD 会发现新版本镜像,触发滚动更新
  4. 旧的 Pod 会被 Terminating,新的 Pod 会启动

29295ce94ad216d1f32b362fc79edb74

验证#

浏览器打开 http://192.168.100.10:30088,看到 “Hello from Westos Harbor - v2” 就说明整条链路通了!

折腾完的一些感受#

搞了这么久,总算把 CI/CD 这套东西跑通了。几个体会:

  1. 存储别省 - 一开始嫌 NFS 麻烦,后来发现不搞持久化根本没法玩
  2. 资源给足 - Jenkins 挺吃资源的,配少了会卡死
  3. GitOps 确实爽 - 改个 YAML 推上去,自动就部署了,再也不用 SSH 上去手动重启容器
  4. 踩坑是常态 - Service 配置、凭证命名空间、容器生命周期,每个都踩了一遍

如果你也在折腾这些东西,有问题欢迎交流。反正我这一路踩的坑够多了,说不定能帮上忙。

从零搭建 K8s CI/CD 流水线实战记录
https://dev-null-sec.github.io/posts/从零搭建-k8s-cicd-流水线实战记录/
作者
DevNull
发布于
2025-05-14
许可协议
CC BY-NC-SA 4.0