搭建 Jenkins + GitLab + Harbor + K8s 全流程 CI/CD
本文目的:在一套离线内网环境中,从零搭建一条"代码提交即自动上线"的完整 CI/CD 流水线——GitLab 管代码、Harbor 存镜像、Jenkins 调 K8s 动态 Agent 跑流水线、Trivy 扫镜像漏洞,最后一条 Webhook 触发全自动发布。
结论先行:只要按本文顺序把 Harbor、GitLab、Jenkins 三套基础设施部署好、把凭据和 K8s 动态 Agent 打通,剩下的核心就是一份约 200 行的
Jenkinsfile——它串联起「拉代码 → 构建镜像 → 漏洞扫描 → 部署上线」四个阶段。整条链路跑通后,开发者只需git push一次代码,其余全部自动化完成。
目录
- 一、环境与整体架构
- 二、部署 Harbor 私有镜像仓库
- 三、部署 GitLab 代码仓库
- 四、K8s 节点信任 Harbor
- 五、预下载并导入 Agent / 基础镜像
- 六、部署 Jenkins
- 七、配置 Jenkins 对接 K8s Cloud
- 八、配置凭据与部署密钥
- 九、编写应用代码与 Jenkinsfile
- 十、创建 Pipeline 并配置 Webhook 自动触发
- 十一、总结
一、环境与整体架构
1.1 服务器规划
本方案共 5 台机器:2 台 Docker 主机负责跑 GitLab / Harbor / Jenkins 等基础设施,3 台 K8s 节点承载应用运行与动态构建 Agent。
| 主机名 | 内网 IP(已脱敏,按需替换) | 角色 |
|---|---|---|
| docker-1 | 192.168.188.30 | Harbor + GitLab |
| docker-2 | 192.168.188.70 | Jenkins |
| K8s-master | 192.168.188.170 | 控制面 |
| K8s-node1 | 192.168.188.171 | 工作节点 |
| K8s-node2 | 192.168.188.172 | 工作节点 |
1.2 完整流程链路
开发者 push 代码
│
▼
GitLab ──(Webhook)──▶ Jenkins
│
├─▶ 拉取代码(GitLab)
├─▶ 构建镜像(K8s 动态 Agent + dind)
├─▶ Trivy 漏洞扫描
├─▶ 推送镜像到 Harbor
└─▶ kubectl 部署到 K8s (production 命名空间)
1.3 技术栈清单
| 组件 | 版本 | 用途 |
|---|---|---|
| Harbor | v2.10.3 | 私有镜像仓库(含 Trivy 扫描) |
| GitLab CE | 16.11.5 | 代码仓库 + Webhook 触发 |
| Jenkins | 2.541.3-jdk17 | CI/CD 编排引擎 |
| Kubernetes | 1.36 | 应用运行 + 动态构建 Agent |
| Trivy | 0.76.1 | 镜像漏洞扫描 |
| Go | 1.27-alpine3.23 | 示例应用语言 |
文中所有密码与内网 IP 已做脱敏处理,实际部署时请替换为自己的环境值。
二、部署 Harbor 私有镜像仓库
在 docker-1 上操作。Harbor 是整套 CI/CD 的镜像中枢,先部署它是后续一切的前提。
2.1 配置 Docker 并下载 Harbor
cat > /etc/docker/daemon.json << 'EOF'
{
"insecure-registries": ["192.168.188.30:8080"],
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me",
"https://docker.m.daocloud.io"
],
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
EOF
systemctl daemon-reload && sudo systemctl restart docker
docker info | grep -A2 -i insecure
# 下载离线安装包
wget https://github.com/goharbor/harbor/releases/download/v2.10.3/harbor-offline-installer-v2.10.3.tgz -O harbor-offline-installer.tgz
tar -xzf harbor-offline-installer.tgz
cd harbor
由于是内网环境,这里直接把 Harbor 地址加入
insecure-registries,跳过 HTTPS 证书配置;生产环境强烈建议启用 HTTPS。
2.2 修改配置并安装
cp harbor.yml.tmpl harbor.yml
sed -i 's/^hostname: reg.mydomain.com/hostname: <HARBOR_IP>/' harbor.yml
sed -i 's/^ port: 80/ port: 8080/' harbor.yml
sed -i 's/^ port: 443/# port: 443/' harbor.yml
sed -i 's/^ certificate: /# certificate: /' harbor.yml
sed -i 's/^ private_key: /# private_key: /' harbor.yml
sed -i 's/^harbor_admin_password: Harbor12345/harbor_admin_password: Root1234/' harbor.yml
./install.sh --with-trivy
docker ps | grep harbor
curl -s http://192.168.188.30:8080/api/v2.0/health
访问:http://192.168.188.30:8080(账号 admin)
2.3 新建项目
登录 Harbor 后,创建一个名为 cicd 的非公开项目,用于存放后续的镜像。

2.4 Harbor 上传测试
# 测试上传
docker pull hello-world
docker tag hello-world 192.168.188.30:8080/cicd/hello-world:latest
docker login 192.168.188.30:8080 -u admin -p Root1234
docker push 192.168.188.30:8080/cicd/hello-world:latest
看到 Pushed 即说明 Harbor 工作正常。
三、部署 GitLab 代码仓库
在 docker-1 上操作。GitLab 负责代码托管,并作为 Webhook 的触发源头。
3.1 编写 docker-compose 启动 GitLab
docker pull gitlab/gitlab-ce:16.11.5-ce.0
mkdir -p /root/gitlab
cd /root/gitlab
cat > /root/gitlab/docker-compose.yml << 'EOF'
services:
gitlab:
image: gitlab/gitlab-ce:16.11.5-ce.0
container_name: gitlab
restart: always
hostname: gitlab.local
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.188.30'
gitlab_rails['gitlab_shell_ssh_port'] = 2222
gitlab_rails['time_zone'] = 'Asia/Shanghai'
prometheus_monitoring['enable'] = false
registry['enable'] = false
nginx['listen_port'] = 80
ports:
- "80:80"
- "2222:22"
volumes:
- /root/gitlab/config:/etc/gitlab
- /root/gitlab/logs:/var/log/gitlab
- /root/gitlab/data:/var/opt/gitlab
shm_size: '256m'
EOF
docker compose up -d
docker compose logs -f --tail=20
# 获取初始 root 密码
docker exec gitlab grep 'Password:' /etc/gitlab/initial_root_password 2>/dev/null
这里把 SSH 端口映射到宿主机的
2222,避免和宿主机自带的 22 端口冲突。
3.2 初始化配置
- 访问
http://192.168.188.30,用root账号登录。 - 切换中文:左上角头像 → Preferences → Preferences → 语言选中文。
- 修改密码:左上角头像 → 偏好设置 → 密码。
- 关闭公开注册:左下角管理中心 → 设置 → 通用 → 注册限制 → 取消勾选「已启用注册功能」。
- 创建项目:左上角
+→ 新建项目/仓库 → 新建空白项目(后续演示用demo-app)。

四、K8s 节点信任 Harbor
在 master、node1、node2 三台 K8s 节点上执行。K8s 用 containerd 拉镜像,需单独配置 Harbor 信任,与 Docker 的
insecure-registries相互独立。
# master、node1、node2 执行
mkdir -p /etc/containerd/certs.d/192.168.188.30:8080
tee /etc/containerd/certs.d/192.168.188.30:8080/hosts.toml >/dev/null <<'EOF'
server = "http://192.168.188.30:8080"
[host."http://192.168.188.30:8080"]
capabilities = ["pull", "resolve"]
skip_verify = true
[host."http://192.168.188.30:8080".header]
authorization = "Basic YWRtaW46Um9vdDEyMzQ="
EOF
# authorization 对应的值是密码,被 base 64 转成了 YWRtaW46Um9vdDEyMzQ=
systemctl restart containerd && systemctl status containerd --no-pager
crictl pull 192.168.188.30:8080/cicd/hello-world:latest
crictl images | grep 192.168.188.30:8080/cicd/hello-world
authorization通过echo -n 'admin:yijiuruchu' | base64生成,用于 containerd 免密拉取私有镜像。
五、预下载并导入 Agent / 基础镜像
在 docker-1 上操作。由于内网访问公网镜像源不稳定,这里先在一台能联网的 Docker 机上统一拉取镜像,再分发到各节点,避免构建时拉镜像失败。
5.1 拉取 Agent 镜像与基础镜像
IMAGES=(
"jenkins/inbound-agent:latest-jdk21"
"docker:27.3.1-dind"
"heyvaldemar/kubectl:1.36"
"golang:1.27-alpine3.23"
"alpine:3.23"
"aquasec/trivy:0.76.1"
"ghcr.io/aquasecurity/trivy-db:2"
)
for img in "${IMAGES[@]}"; do docker pull --platform linux/amd64 "$img"; done
for img in "${IMAGES[@]}"; do docker images | grep "$img"; done
5.2 导出并导入 K8s 节点
# 打包 Agent 相关镜像
docker save \
jenkins/inbound-agent:latest-jdk21 \
docker:27.3.1-dind \
heyvaldemar/kubectl:1.36 \
-o /root/jenkins-agent-images.tar
# 分发到三台 K8s 节点并导入
for ip in 192.168.188.170 192.168.188.171 192.168.188.172; do
scp /root/jenkins-agent-images.tar root@$ip:/root/
ssh root@$ip "ctr -n k8s.io images import /root/jenkins-agent-images.tar"
done
5.3 基础镜像推入 Harbor
# 登录
docker login 192.168.188.30:8080 -u admin -p Root1234
# tag
docker tag golang:1.27-alpine3.23 192.168.188.30:8080/cicd/golang:1.27-alpine3.23
docker tag alpine:3.23 192.168.188.30:8080/cicd/alpine:3.23
docker tag aquasec/trivy:0.76.1 192.168.188.30:8080/cicd/trivy:0.76.1
docker tag ghcr.io/aquasecurity/trivy-db:2 192.168.188.30:8080/cicd/trivy-db:2
# 上传
docker push 192.168.188.30:8080/cicd/golang:1.27-alpine3.23
docker push 192.168.188.30:8080/cicd/alpine:3.23
docker push 192.168.188.30:8080/cicd/trivy:0.76.1
docker push 192.168.188.30:8080/cicd/trivy-db:2
六、部署 Jenkins
在 docker-2 上操作。Jenkins 是整个流水线的调度中枢。
6.1 配置 Docker 并拉取镜像
cat > /etc/docker/daemon.json << 'EOF'
{
"insecure-registries": ["192.168.188.30:8080"],
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me",
"https://docker.m.daocloud.io"
],
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
EOF
systemctl daemon-reload && systemctl restart docker
docker pull --platform linux/amd64 jenkins/jenkins:2.541.3-jdk17
mkdir -p /root/jenkins
6.2 编写 docker-compose 启动 Jenkins
cat > /root/jenkins/docker-compose.yml <<'EOF'
services:
jenkins:
image: jenkins/jenkins:2.541.3-jdk17
container_name: jenkins
restart: always
user: root
environment:
JAVA_OPTS: "-Xms512m -Xmx2g"
ports:
- "8080:8080"
- "50000:50000"
volumes:
- /root/jenkins/data:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
shm_size: "256m"
deploy:
resources:
limits:
memory: 4G
EOF
# 放行 Agent 回连端口
ufw allow 8080/tcp
ufw allow 50000/tcp
ufw reload
# 启动 Jenkins
cd /root/jenkins
docker compose up -d
# 获取初始管理员密码
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
6.3 初始化与插件安装
- 访问
http://192.168.188.70:8080,输入初始密码,选择「安装推荐的插件」。 - 创建管理员账号(用户名:root,密码:yijiuruchu、邮箱按需填写)。
- 切换暗色主题:右上角头像 → Preferences → Appearance → 选暗色主题。
- 安装必要插件:右上角设置 → 插件管理 → Available plugins,安装以下插件:
- Kubernetes
- Gitlab
- Docker Pipeline
7. 配置 Jenkins 对接 K8s Cloud
Jenkins 通过 Kubernetes 插件在 K8s 中动态拉起构建 Agent Pod,构建完自动销毁,这是整套方案的核心亮点——不再需要固定 Agent 节点。
7.1 K8s 侧准备(master 执行)
# 获取 CA 证书(配置 Jenkins 时要用)
kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d
kubectl create namespace jenkins # 创建命名空间
kubectl -n jenkins create serviceaccount jenkins-agent # 创建账号
# 赋予 Agent 操作 Pod 的权限
kubectl -n jenkins create role jenkins-agent-role \
--verb=get,list,watch,create,delete,patch,update \
--resource=pods,pods/exec,pods/log,secrets,configmaps
kubectl -n jenkins create rolebinding jenkins-agent \
--role=jenkins-agent-role --serviceaccount=jenkins:jenkins-agent
# 生成 10 年有效期的 Token
kubectl -n jenkins create token jenkins-agent --duration=87600h
生成 10 年 Token 是为了省去反复续期;生产环境建议缩短有效期并配合轮换机制。
7.2 Jenkins 侧配置 Cloud
右上角设置 → Clouds → new clouds → Cloud name:k8s(勾选「固定节点」),填写:
- 名称:
k8s - Kubernetes 地址:
https://192.168.188.170:6443 - Kubernetes 服务证书 key:上面获取的 CA 证书
- Kubernetes 命名空间:
jenkins - 凭证:添加 → Secret text(全局,secret 填 10 年 Token,ID 建议
k8s-agent-token) - Jenkins 地址:
http://192.168.188.70:8080 - 容器数量:5

八、配置凭据与部署密钥
凭据是 Jenkins 连接 GitLab / Harbor / K8s 的「钥匙」,务必在流水线跑之前配齐。
8.1 配置 GitLab / Harbor 凭据
右上角设置 → 凭证管理 → 添加凭证,共两个:
| 凭证 | 类型 | 范围 | Username | Password | ID |
|---|---|---|---|---|---|
| Harbor 凭证 | Username with password | 全局 | admin | Root1234 | harbor-credentials |
| GitLab 凭证 | Username with password | 全局 | root | yijiuruchu | gitlab-credentials |
8.2 创建 K8s 部署凭据(production 命名空间)
kubectl create namespace production
kubectl -n production create serviceaccount jenkins-deployer
# verb 必须包含 create——首次部署 kubectl apply 不存在的资源需要 create 权限,
# 否则报 Forbidden (cannot create resource "deployments")
kubectl -n production create role jenkins-deployer \
--verb=get,list,create,patch,update,watch \
--resource=deployments,deployments/rollback,replicasets,services,configmaps
kubectl -n production create rolebinding jenkins-deployer \
--role=jenkins-deployer --serviceaccount=production:jenkins-deployer
# 用 kubectl create token 生成长期 token
# (K8s 1.24+ 手工创建 Secret 会被回收,导致部署时 401 Unauthorized)
# --duration 设 10 年近似永久
TOKEN=$(kubectl -n production create token jenkins-deployer --duration=87600h)
APISERVER="https://192.168.188.170:6443"
CA_DATA=$(kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')
cat > kubeconfig-deployer <<EOF
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: ${CA_DATA}
server: ${APISERVER}
name: production
contexts:
- context:
cluster: production
namespace: production
user: jenkins-deployer
name: deployer
current-context: deployer
users:
- name: jenkins-deployer
user:
token: ${TOKEN}
EOF
# 将 kubeconfig-deployer 文件下载,再上传为 Jenkins 凭据
将生成的 kubeconfig-deployer 上传为 Jenkins 凭据:
- 右上角设置 → 凭证管理 → 添加凭证 → 类型:Secret file
- 范围:全局
- File:上传
kubeconfig-deployer文件 - ID:
kubeconfig - 描述:K8s 部署凭据
8.3 创建 Harbor 拉取密钥
# jenkins 命名空间(Agent Pod 用)
kubectl -n jenkins create secret docker-registry harbor-regcred \
--docker-server=192.168.188.30:8080 \
--docker-username=admin \
--docker-password='Root1234'
# production 命名空间(应用镜像用)
kubectl -n production create secret docker-registry harbor-regcred \
--docker-server=192.168.188.30:8080 \
--docker-username=admin \
--docker-password='Root1234'
# 验证
kubectl -n jenkins get secret harbor-regcred
kubectl -n production get secret harbor-regcred
九、编写应用代码与 Jenkinsfile
准备一个 Go 演示应用,连同 Jenkinsfile 一起推到 GitLab,作为流水线的最小可运行示例。
本节的几个文件共同构成一套完整的"代码 → 镜像 → 部署"闭环,它们的关联如下:
go.mod/main.go:应用的源代码,是流水线的输入。Dockerfile:把源码编译成可运行镜像的配方,供 Jenkins 构建阶段使用。deploy/production.yaml:应用在 K8s 里的部署清单,定义几个副本、怎么暴露端口。Jenkinsfile:流水线的总指挥,把前三个文件串起来按阶段自动执行。
go.mod + main.go ──(被 Dockerfile 引用编译)──▶ Dockerfile ──(构建)──▶ demo-app 镜像
│
deploy/production.yaml ──(引用 demo-app 镜像)──▶ K8s Deployment + Service
│
Jenkinsfile ──(按顺序驱动上述所有步骤)──▶ 拉代码 → 构建 → 扫描 → 部署
9.1 授权 namespace 读取
kubectl create clusterrole jenkins-deployer-ns-reader \
--verb=get --resource=namespaces
kubectl create clusterrolebinding jenkins-deployer-ns-reader \
--clusterrole=jenkins-deployer-ns-reader \
--serviceaccount=production:jenkins-deployer
作用:给部署账号
jenkins-deployer追加一个只读namespaces的集群权限。因为 Jenkinsfile 部署阶段会先执行kubectl get ns production探测 API Server 是否可达,若账号连 namespace 的 get 权限都没有,这一步会直接失败。
9.2 初始化仓库
mkdir -p /root/demo-app/deploy
cd /root/demo-app
git init
git config --global user.email "root@example.com"
git config --global user.name "root"
git remote add origin http://192.168.188.30/root/demo-app.git
git branch -M main
作用:初始化本地 Git 仓库并关联到 GitLab 上的
demo-app空项目,作为后续所有文件的载体。这里提前建好deploy/目录,用于存放 K8s 部署清单。
9.3 应用源代码(go.mod 与 main.go)
# go.mod —— 声明模块名与 Go 版本
cat > go.mod << 'EOF'
module demo-app
go 1.22
EOF
# main.go —— 一个极简 HTTP 服务
cat > main.go << 'EOF'
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, this is my Demo App running in K8s!")
})
fmt.Println("Server starting on port 8080...")
http.ListenAndServe(":8080", nil)
}
EOF
作用:这是被部署的"业务本体"。
go.mod声明模块名demo-app和 Go 版本 1.22;main.go用标准库net/http起了一个监听 8080 端口的 HTTP 服务,访问根路径返回一段问候文本。关键逻辑:整个程序不依赖任何第三方库(只用标准库),因此后续 Dockerfile 里只需
COPY go.mod main.go两个文件就能编译,无需go mod download,构建快且离线环境无网络依赖。关联:这两个文件是
Dockerfile的编译输入。
9.4 镜像构建配方(Dockerfile)
# Dockerfile —— 多阶段构建,减小镜像体积
cat > Dockerfile << 'EOF'
FROM 192.168.188.30:8080/cicd/golang:1.27-alpine3.23 AS builder
WORKDIR /app
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o demo-app .
FROM 192.168.188.30:8080/cicd/alpine:3.23
WORKDIR /app
COPY --from=builder /app/demo-app .
EXPOSE 8080
CMD ["./demo-app"]
EOF
作用:把 Go 源码编译成可运行的容器镜像,并采用多阶段构建把最终镜像压到最小。
关键逻辑(两个阶段):
- builder 阶段:基于 Harbor 里的
golang:1.27-alpine3.23基础镜像编译程序。CGO_ENABLED=0关闭 CGO 生成纯静态二进制,-trimpath去掉编译路径、-ldflags="-s -w"剥离符号表和调试信息,三重手段让产物更小更干净。- 运行阶段:切换到只有几 MB 的
alpine:3.23,用COPY --from=builder只把编译好的二进制拷进来,丢弃整个 Go 工具链。这样最终镜像里没有源码、没有编译器,体积小、攻击面也小。关联:基础镜像
golang和alpine都来自 Harbor(第五章已预推送);产物demo-app镜像会被 Jenkins 打上 commit 短哈希 tag 后推回 Harbor。
9.5 部署清单(deploy/production.yaml)
cat > deploy/production.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
imagePullSecrets:
- name: harbor-regcred
containers:
- name: demo-app
image: 192.168.188.30:8080/cicd/demo-app:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: demo-app
namespace: production
spec:
type: NodePort
selector:
app: demo-app
ports:
- port: 8080
targetPort: 8080
nodePort: 30080
EOF
作用:定义应用在 K8s 里的运行形态——跑 2 个副本、从私有 Harbor 拉镜像、通过 NodePort 对外暴露。
关键逻辑:
replicas: 2:两个副本保证高可用,任一节点挂了流量不中断。imagePullSecrets: harbor-regcred:引用第八章在production命名空间创建的拉取密钥,让 K8s 能免密从 Harbor 拉私有镜像。image: .../demo-app:latest:这里的latest只是占位,真正部署时 Jenkins 会把它替换成commit 短哈希版本。- Service 用
NodePort: 30080,集群任意节点访问30080端口即可直达服务,方便内网测试。关联:
imagePullSecrets依赖第八章的harbor-regcred;image字段会被 Jenkinsfile 的部署阶段动态改写。
9.6 Jenkinsfile(流水线核心)
Jenkinsfile是整套方案的大脑,全文约 200 行,一句话概括:在 K8s 里拉起一个临时 Pod 当构建环境,然后在里面依次执行"拉代码 → 构建推送镜像 → Trivy 扫描 → 部署上线"四个阶段。 下面按模块拆开讲。
整体骨架
cat > /root/demo-app/Jenkinsfile << 'EOF'
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
imagePullSecrets:
- name: harbor-regcred
initContainers:
- name: kubectl-bin
image: heyvaldemar/kubectl:1.36
imagePullPolicy: IfNotPresent
securityContext:
runAsUser: 0
command: ["/bin/sh", "-c"]
args:
- |
set -e
KUBECTL_PATH=$(command -v kubectl)
echo "found kubectl at: $KUBECTL_PATH"
cp "$KUBECTL_PATH" /home/jenkins/agent/kubectl
chmod 755 /home/jenkins/agent/kubectl
/home/jenkins/agent/kubectl version --client
volumeMounts:
- mountPath: /home/jenkins/agent
name: workspace-volume
containers:
- name: jnlp
image: jenkins/inbound-agent:latest-jdk21
imagePullPolicy: IfNotPresent
tty: true
workingDir: /home/jenkins/agent
- name: docker
image: docker:27.3.1-dind
imagePullPolicy: IfNotPresent
securityContext:
privileged: true
tty: true
workingDir: /home/jenkins/agent
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
command: ["/bin/sh", "-c"]
args:
- |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<'DOCKERD_EOF'
{
"insecure-registries": ["192.168.188.30:8080"],
"mtu": 1360
}
DOCKERD_EOF
exec dockerd-entrypoint.sh --host=tcp://0.0.0.0:2375
env:
- name: DOCKER_TLS_CERTDIR
value: ""
volumes:
- emptyDir: {}
name: workspace-volume
'''
}
}
environment {
HARBOR_ADDR = '192.168.188.30:8080'
REGISTRY = '192.168.188.30:8080/cicd'
APP_NAME = 'demo-app'
DOCKER_HOST = 'tcp://localhost:2375'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://192.168.188.30/root/demo-app.git',
credentialsId: 'gitlab-credentials'
}
}
stage('Build & Push Image') {
steps {
container('docker') {
withCredentials([usernamePassword(credentialsId: 'harbor-credentials',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS')]) {
sh """
AUTH=\$(printf '%s:%s' "\${HARBOR_USER}" "\${HARBOR_PASS}" | base64 -w 0)
mkdir -p ~/.docker
printf '{"auths":{"${HARBOR_ADDR}":{"auth":"%s"}}}' "\$AUTH" > ~/.docker/config.json
docker pull ${REGISTRY}/golang:1.27-alpine3.23
docker pull ${REGISTRY}/alpine:3.23
docker build -t ${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)} .
docker push ${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)}
"""
}
}
}
}
stage('Scan Image') {
steps {
container('docker') {
withCredentials([usernamePassword(credentialsId: 'harbor-credentials',
usernameVariable: 'HARBOR_USER',
passwordVariable: 'HARBOR_PASS')]) {
sh """
docker run --rm \\
-e TRIVY_DB_REPOSITORY=192.168.188.30:8080/cicd/trivy-db:2 \\
192.168.188.30:8080/cicd/trivy:0.76.1 image --insecure \\
--username \${HARBOR_USER} --password \${HARBOR_PASS} \\
--severity HIGH,CRITICAL --exit-code 1 \\
${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)}
"""
}
}
}
}
stage('Deploy to Production') {
steps {
container('docker') {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG_FILE')]) {
sh """
set -eux
KUBECTL=/home/jenkins/agent/kubectl
ls -l \$KUBECTL
echo "STEP 1: kubectl client version"
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} version --client --request-timeout=10s
echo "STEP 2: apiserver reachable"
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} get ns production --request-timeout=15s
echo "STEP 3: deploy"
if \$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} -n production \\
get deployment ${APP_NAME} --request-timeout=15s >/dev/null 2>&1; then
echo "deployment exists, set image only"
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} -n production \\
set image deployment/${APP_NAME} \\
${APP_NAME}=${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)} \\
--request-timeout=30s
else
echo "first deploy: apply + set image"
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} -n production \\
apply -f deploy/production.yaml --request-timeout=30s
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} -n production \\
set image deployment/${APP_NAME} \\
${APP_NAME}=${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)} \\
--request-timeout=30s
fi
echo "STEP 4: rollout status"
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} -n production \\
rollout status deployment/${APP_NAME} --timeout=120s
"""
}
}
}
}
}
post {
success { echo 'Pipeline succeeded.' }
failure { echo 'Pipeline failed.' }
}
}
EOF
拆解骨架各层含义
cat > /root/demo-app/Jenkinsfile << 'EOF'
pipeline {
agent {
kubernetes {
yaml '''... Pod 定义 ...'''
}
}
environment { ... 全局变量 ... }
stages { ... 四个 stage ... }
post { ... 结束钩子 ... }
}
EOF
作用:这是一份声明式(Declarative)流水线。最外层
pipeline由五个块组成:
agent:指定用什么环境跑流水线,这里用kubernetes插件在 K8s 里动态建一个 Pod。environment:定义全局变量,供各阶段引用。stages:真正的执行步骤(四个 stage)。post:流水线结束后(无论成败)的回调。
agent 动态 Pod 定义(核心)
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
imagePullSecrets:
- name: harbor-regcred
initContainers:
- name: kubectl-bin
image: heyvaldemar/kubectl:1.36
command: ["/bin/sh", "-c"]
args:
- |
set -e
KUBECTL_PATH=$(command -v kubectl)
cp "$KUBECTL_PATH" /home/jenkins/agent/kubectl
chmod 755 /home/jenkins/agent/kubectl
volumeMounts:
- mountPath: /home/jenkins/agent
name: workspace-volume
containers:
- name: jnlp
image: jenkins/inbound-agent:latest-jdk21
workingDir: /home/jenkins/agent
- name: docker
image: docker:27.3.1-dind
securityContext:
privileged: true
command: ["/bin/sh", "-c"]
args:
- |
cat > /etc/docker/daemon.json <<'DOCKERD_EOF'
{ "insecure-registries": ["192.168.188.30:8080"], "mtu": 1360 }
DOCKERD_EOF
exec dockerd-entrypoint.sh --host=tcp://0.0.0.0:2375
env:
- name: DOCKER_TLS_CERTDIR
value: ""
volumes:
- emptyDir: {}
name: workspace-volume
'''
}
}
作用:这是整套方案最关键的差异化设计——不再用固定 Agent 机器,而是每次构建都在 K8s 里临时起一个 Pod,跑完自动销毁。
关键逻辑(这个 Pod 里有三个"角色"):
initContainers: kubectl-bin:构建前先把kubectl二进制从专用镜像拷到共享卷workspace-volume,供后面部署阶段使用。用 initContainer 而非业务容器,是为了"先准备好工具、再干活"。containers: jnlp:Jenkins 的官方 Agent 容器(inbound-agent),是 Jenkins 主节点与 Pod 通信的桥梁,构建指令都发到它这里执行。containers: docker:跑了一个 Docker-in-Docker(dind),即"在 Pod 里再起一个 Docker 守护进程",通过--host=tcp://0.0.0.0:2375暴露给其他容器调用,用来做docker build/docker push。配置insecure-registries让它能访问内网 HTTP 的 Harbor。三个容器通过
emptyDir共享卷workspace-volume共享工作目录和 kubectl 工具,imagePullSecrets: harbor-regcred让 Pod 能拉 Harbor 里的私有镜像。关联:Pod 里的
docker(dind)、kubectl、harbor-regcred分别对应第五章预导入的镜像和第八章的拉取密钥,缺一构建就会卡在拉镜像环节。
全局变量
environment {
HARBOR_ADDR = '192.168.188.30:8080'
REGISTRY = '192.168.188.30:8080/cicd'
APP_NAME = 'demo-app'
DOCKER_HOST = 'tcp://localhost:2375'
}
作用:把 Harbor 地址、镜像仓库路径、应用名、Docker 守护进程地址集中定义成变量,后续各阶段直接引用,改环境时只改这一处。
关键逻辑:
DOCKER_HOST = 'tcp://localhost:2375'告诉 Agent 里的 docker 客户端去连同一 Pod 里 dind 容器暴露的 2375 端口,实现"无本地 Docker 也能构建"。
四个 stage 详解
Stage 1 — Checkout(拉代码)
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://192.168.188.30/root/demo-app.git',
credentialsId: 'gitlab-credentials'
}
}
作用:从 GitLab 拉取
main分支代码到工作目录,是整个流水线的起点。
关联:credentialsId: 'gitlab-credentials'引用第八章配置的 GitLab 凭据,用于私有仓库鉴权。
Stage 2 — Build & Push Image(构建并推送镜像)
stage('Build & Push Image') {
steps {
container('docker') {
withCredentials([usernamePassword(credentialsId: 'harbor-credentials', ...)]) {
sh """
AUTH=\$(printf '%s:%s' "\${HARBOR_USER}" "\${HARBOR_PASS}" | base64 -w 0)
printf '{"auths":{"${HARBOR_ADDR}":{"auth":"%s"}}}' "\$AUTH" > ~/.docker/config.json
docker pull ${REGISTRY}/golang:1.27-alpine3.23
docker pull ${REGISTRY}/alpine:3.23
docker build -t ${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)} .
docker push ${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)}
"""
}
}
}
}
作用:在 dind 容器里构建镜像并推送到 Harbor。
关键逻辑:
container('docker'):指定在 Pod 里的docker容器中执行本阶段。withCredentials:从 Jenkins 凭据库取出 Harbor 账号密码,注入HARBOR_USER/HARBOR_PASS环境变量。- 先把账号密码拼成
auth字段写进~/.docker/config.json,实现免密 push。${env.GIT_COMMIT.take(7)}:取当前提交哈希前 7 位作为镜像 tag,做到每次构建镜像版本可追溯、不互相覆盖。- 先
docker pull两个基础镜像,是因为它们已提前存进 Harbor,构建时直接用缓存,离线也能秒级完成。
关联:docker build会读取 9.4 节的Dockerfile,产物镜像推回 Harbor 供下一步扫描和部署。
Stage 3 — Scan Image(漏洞扫描)
stage('Scan Image') {
steps {
container('docker') {
withCredentials([usernamePassword(credentialsId: 'harbor-credentials', ...)]) {
sh """
docker run --rm \\
-e TRIVY_DB_REPOSITORY=192.168.188.30:8080/cicd/trivy-db:2 \\
192.168.188.30:8080/cicd/trivy:0.76.1 image --insecure \\
--username \${HARBOR_USER} --password \${HARBOR_PASS} \\
--severity HIGH,CRITICAL --exit-code 1 \\
${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)}
"""
}
}
}
}
作用:用 Trivy 扫描刚构建的镜像,检查 HIGH 和 CRITICAL 级别的漏洞,实现"安全左移"——在部署前就把高危问题拦下。
关键逻辑:
--severity HIGH,CRITICAL:只关注高危和严重漏洞,避免低危告警刷屏。--exit-code 1:一旦命中指定级别漏洞,Trivy 返回非零退出码,整个流水线立刻失败,镜像不会被部署。TRIVY_DB_REPOSITORY指向 Harbor 里的trivy-db,离线环境也能更新漏洞库。
关联:扫描的镜像正是 Stage 2 刚推送的那个(同一个 commit tag),形成"构建 → 扫描"的流水接力。
Stage 4 — Deploy to Production(部署上线)
stage('Deploy to Production') {
steps {
container('docker') {
withCredentials([file(credentialsId: 'kubeconfig', variable: 'KUBECONFIG_FILE')]) {
sh """
set -eux
KUBECTL=/home/jenkins/agent/kubectl
echo "=== STEP 1: kubectl client version ==="
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} version --client --request-timeout=10s
echo "=== STEP 2: apiserver reachable ==="
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} get ns production --request-timeout=15s
echo "=== STEP 3: deploy ==="
if \$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} -n production \\
get deployment ${APP_NAME} --request-timeout=15s >/dev/null 2>&1; then
# 已存在:只更新镜像
\$KUBECTL ... set image deployment/${APP_NAME} \\
${APP_NAME}=${REGISTRY}/${APP_NAME}:${env.GIT_COMMIT.take(7)} ...
else
# 首次部署:先 apply 清单再更新镜像
\$KUBECTL ... apply -f deploy/production.yaml ...
\$KUBECTL ... set image deployment/${APP_NAME} ...
fi
echo "=== STEP 4: rollout status ==="
\$KUBECTL --kubeconfig=\${KUBECONFIG_FILE} -n production \\
rollout status deployment/${APP_NAME} --timeout=120s
"""
}
}
}
}
作用:用 kubectl 把新镜像部署到 K8s 的
production命名空间。
关键逻辑(四个 STEP):
- STEP 1/2:先探测 kubectl 可用性和 API Server 连通性,把连接问题提前暴露,避免部署时才报错。
- STEP 3(核心分支):判断
demo-app这个 Deployment 是否已存在——已存在就只set image更新镜像(快、无中断);不存在则先apply -f创建资源再更新镜像。这个if/else是"首次部署 vs 增量更新"的关键区分。- STEP 4:
rollout status阻塞等待滚动更新完成(最多 120 秒),确保部署真正成功才结束。withCredentials([file(...)]):把第八章上传的kubeconfig凭据文件注入环境变量,实现免密访问 K8s。
关联:deploy/production.yaml(9.5 节)在首次部署时被apply;kubectl二进制来自 9.6.2 节 initContainer 预拷贝到共享卷的产物。
post 结束钩子
post {
success { echo 'Pipeline succeeded.' }
failure { echo 'Pipeline failed.' }
}
作用:流水线结束时按结果回显一句日志,便于在构建控制台快速判断成败。实际项目可在这里扩展为发送企业微信/邮件通知、失败回滚等逻辑。
9.7 推送代码到 GitLab
git add .
git commit -m "完成"
git pull origin main --allow-unrelated-histories --no-rebase # 按 Ctrl + X
git push -u origin main
作用:把上述所有文件(源码、Dockerfile、部署清单、Jenkinsfile)一次性推送到 GitLab。
--allow-unrelated-histories用于合并远程仓库可能已有的初始提交(如初始化时自动生成的 README),避免历史无关导致的拒绝合并。推送成功后,即可进入下一章创建 Pipeline 并让 Webhook 自动触发构建。
十、创建 Pipeline 并配置 Webhook 自动触发
10.1 创建 Pipeline 任务
Jenkins 首页 → 新建任务:
- 名称:
demo-app-pipeline - 类型:流水线
进入配置 → 流水线 → 选择「Pipeline script from SCM」:
- SCM:Git
- Repository URL:
http://192.168.188.30/root/demo-app.git - Credentials:
gitlab-credentials - Branches to build:
*/main - 脚本路径:
Jenkinsfile

10.2 配置 Webhook 自动触发
Jenkins:


GitLab:
- 配置出站请求白名单:主页 → 管理中心 → 设置 → 网络 → 出站请求,勾选允许到本机/私网地址。

- 添加 Webhook:项目 → 设置 → webhooks → 添加 webhooks,URL 填 Jenkins 生成的地址,勾选 Push events。



推送一次代码,Jenkins 会自动收到事件并触发构建,全流程跑通即代表 CI/CD 链路闭环。
十一、总结
本文从零搭建了一套完整的离线环境 CI/CD 流水线,核心链路为:
GitLab(代码)→ Webhook → Jenkins → K8s 动态 Agent → 构建镜像 → Trivy 扫描 → Harbor 存储 → kubectl 部署上线。
几个关键设计点:
- K8s 动态 Agent:Jenkins 用 Kubernetes 插件在集群内按需拉起构建 Pod,构建完自动回收,省去固定 Agent 的运维成本。
- 离线镜像预导入:内网环境下先统一拉镜像再分发到各节点,规避构建时拉取公网镜像失败。
- Trivy 左移扫描:在推送镜像前先扫 HIGH/CRITICAL 漏洞,命中即中断流水线,把安全问题挡在上线前。
- 最小权限分离:Jenkins 的 Agent 账号与生产部署账号分离,各自只授必要 RBAC 权限。
可进一步优化方向:Harbor 启用 HTTPS、Secret 改用外置密钥管理(如 Vault)、Webhook Secret 校验、多环境(test/staging/production)流水线拆分、镜像 tag 用语义化版本替代 commit 短哈希。
本文所有密码与内网 IP 均为示例,实际部署请替换为你的环境值,并避免将真实密钥提交到代码仓库。
更多推荐


所有评论(0)