DevOps平台搭建部署流程

DevOps平台搭建部署流程

1.DevOps整体流程

Kubernetes 中进行微服务的 CI/CD 自动化流水线,一般会有如下步骤:

  1. 在 GitLab 中创建对应的项目(一个微服务建议对应一个gitlab项目仓库,项目名称建议和微服务名称保持一致);
  2. 配置 Jenkins 集成 Kubernetes 集群,后期 Jenkins 的 Slave 将为在 Kubernetes 中动态创建的 Slave;
  3. Jenkins 创建对应的任务(Job),一个微服务建议创建一个Jenkins流水线工程,集成该项目的 Git 地址和Kubernetes 集群;
  4. 开发者将代码提交到GitLab;
  5. 如有配置钩子,推送(Push)代码会自动触发 Jenkins 构建,如没有配置钩子,需要手动构建;
  6. Jenkins 控制 Kubernetes(使用的是 Kubernetes 插件)创建 Jenkins Slave(Pod 形式);
  7. Jenkins Slave 根据流水线(Pipeline)定义的步骤执行构建;
  8. 通过 Dockerfile 生成镜像;
  9. 将镜像推送(Push)到私有仓库 Harbor(或者其它的镜像仓库);
  10. Jenkins 再次控制Kubernetes 进行最新的镜像部署;
  1. 流水线结束删除 Jenkins Slave

2.DevOps平台设计:

3.Gitlab安装部署

GitLab 是企业内经常用于代码的版本控制,也是 DevOps 平台中尤为重要的一个工具,接下来单独一台服务器(4C4G40G 以上)上安装 GitLab(如果已有可用的 GitLab,无需安装)

3.1服务器信息

基本信息如下:

主机名 ip 系统信息 gitlab版本 备注
gitlab 192.168.0.222 CentOS Linux release 7.9.2009 (Core) gitlab-ce-16.9.9-ce.0.el7.x86_64.rpm 与k8s集群在同一个局域网

3.2Gitlab安装

关闭服务器的防火墙和selinux:

1
2
3
4
# setenforce 0
# systemctl disable --now firewalld
# vi /etc/sysconfig/selinux 或者 /etc/selinux/config (两个文件是相同的内容,前者是软连接)
把 SELINUX 改为 SELINUX=disabled

首先在 GitLab 国内源下载 GitLab 的安装包:https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-16.9.9-ce.0.el7.x86_64.rpm

将下载后的rpm 包,上传到服务器,之后通过 yum 直接安装即可:

1
2
3
4
5
6
7
[root@gitlab ~]# ll
总用量 1093988
-rw-------. 1 root root 1353 1月 25 2025 anaconda-ks.cfg
-rw-r--r--. 1 root root 1120239596 1月 25 2025 gitlab-ce-16.9.9-ce.0.el7.x86_64.rpm

#也可以直接在服务器上使用wget命令下载
wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-16.9.9-ce.0.el7.x86_64.rpm
1
2
#安装上传好的rpm包
yum install gitlab-ce-16.9.9-ce.0.el7.x86_64.rpm -y

安装完成后需要修改一些配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#修改gitlab的url地址,将external_url 参数的地址改为主机的ip地址
[root@gitlab ~]# vim /etc/gitlab/gitlab.rb
30 ##! address from AWS. For more details, see:
31 ##! https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
##! external_url 'http://gitlab.example.com' 如果要配置域名按照这一行修改
external_url 'http://192.168.0.222'
34 ## Roles for multi-instance GitLab


#大部分公司内可能已经有了 Prometheus 监控平台,所以 GitLab 自带的Prometheus 可以无需安装,后期可以安装GitLab Exporter 即可进行监控。关闭 Prometheus 插件(可选)
[root@gitlab ~]# vim /etc/gitlab/gitlab.rb
1814 prometheus['enable'] = false #这里的值原来是true,我改为了false
1815 # prometheus['monitor_kubernetes'] = true
1816 # prometheus['username'] = 'gitlab-prometheus'

更改完成后需要重新加载配置文件:

1
2
3
4
5
6
#重新下载配置文件
gitlab-ctl reconfigure


#执行启动命令
gitlab-ctl restart

之后可以通过浏览器访问 GitLab,账号 root,第一次登录的密码默认在**/etc/gitlab/initial_root_password**

登录后,开启 import 功能,允许gitlab从外部仓库导入代码到 Gitlab:

[管理中心]-[设置]-[通用]-[导入和导出设置]-[按需勾选]-[保存更改]

3.3测试gitlab

登录后,可以创建一个测试项目,进行一些简单的测试。首先创建一个组,群组名为 test(公司内部组名建议使用项目名称),类型为 私有,之后点击 创建群主 即可:

之后在该组下创建一个 项目

3.4ssh免密推拉代码

拉取gitlab代码有ssh和http两种方式,推荐使用ssh的方式拉取代码,在实际的工作中会遇到以下几种场景:

  • 公司的开发人员拉取推送代码
  • 开发人员借助开发者工具(idea,vscode)拉取
  • CI/CD时Jenkins获取代码

上述三种场景的核心逻辑是一样的:SSH 基于密钥对认证,私钥留在客户端,公钥注册到 GitLab,两者匹配才能通信。只是三种场景的”客户端”不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 在客户端上生成密钥对,-C参数允许你为密钥添加一个注释(comment)。这个注释通常用于标识密钥的用途或所有者。
$ ssh-keygen.exe -t rsa -C "1223584670@qq.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/c/Users/Administrator/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /c/Users/Administrator/.ssh/id_rsa
Your public key has been saved in /c/Users/Administrator/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:I7/xTwy/cRUg7HEfX4GfntS7bKnwYFJ1DlFotvlJ1AU 1223584670@qq.com
The key's randomart image is:
+---[RSA 3072]----+
| .. oE+=|
| o.*ooo|
| . =o*o*|
| ..o+*+|
| . S .. =.=|
| o ..+ B |
| o. += o o|
| +o.++ = |
| . ..ooo |
+----[SHA256]-----+

#生成的密钥位置在/c/Users/Administrator/.ssh/路径,id_rsa私钥,id_rsa.pub公钥
$ ll -a
total 54
drwxr-xr-x 1 Administrator 197121 0 Mar 21 22:57 ./
drwxr-xr-x 1 Administrator 197121 0 Dec 2 12:36 ../
-rw-r--r-- 1 Administrator 197121 111 Mar 1 2025 config
-rw-r--r-- 1 Administrator 197121 2602 Mar 21 22:57 id_rsa
-rw-r--r-- 1 Administrator 197121 571 Mar 21 22:57 id_rsa.pub #公钥,配置到gitlab上
-rw-r--r-- 1 Administrator 197121 4137 Jul 12 2025 known_hosts


#复制公钥的内容放在 GitLab 中:
Administrator@hello MINGW64 ~/.ssh
$ cat id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3j3Q6miMEYE8fIaqKT2eIaCsR7YBrhA5hGWgFaeG69vqMqItI9vd/wLnCmgenrEreRL08Rru2qAnK8BiANat1OfjuuNrOV8xBY3Due4pkEzD5pd/eN49n45KbiDAgQeP65N3Sau5J6nnjMgA7r2XsPszGbJwkwfXCux2IcEMg0JrzXFryMcYcmaA5b6VLVqGFY46PhoATvZBOcvx9pEIRqxuyLwr7SQfugBXwUwxSk85dGLd7X6+Y19cyIbLRuTm81eWlzVRK0uFFZWqeaERkdBE/zfkAXd+qtucuwPCM4u7KQkt8zmfVzl/VIdgLDC2jsOuaoUS97pdCB9uVV2TIEd6H9ktzucNy4AdZ5Dt82D1z9Of/1A4KdXbkguEHrNsBQl5ukpPg8rAYgInKlwptnUT8Jph3nvYEt64S+EGq54qU55W/Dnkwew75nqZFcr3ZWoD6qkCniyAtOsUBkbzbNIWf1nEnp5Q7M4B57hDAvEQg03cbysHT/rsnWDunrbM= 1223584670@qq.com

添加密钥后就可以在客户端通过ssh免密的形式拉取代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建一个测试目录用来存放仓库的代码文件,拉取3.3步骤中创建的test-project项目
k8s从零建设DevOps平台/gitlab-test
$ git clone git@192.168.0.222:test/test-project.git
Cloning into 'test-project'...
The authenticity of host '192.168.0.222 (192.168.0.222)' can't be established.
ED25519 key fingerprint is SHA256:K1ZJkg/A870nzCaBi6d7iK/m+hHwfptsdYOao1gCzIo.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.0.222' (ED25519) to the list of known hosts.
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.


$ cd test-project/
$ ll
total 8
-rw-r--r-- 1 Administrator 197121 6267 Mar 21 23:14 README.md


接下来创建几个文件,然后提交到仓库中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 提交前先配置自己的邮箱和姓名信息,因为 Git 把“谁做了这个改动”永久记录在每一次 commit 里,没有姓名和邮箱就无法识别作者!)
#如果在commit时,遇到下面信息表示没有设置邮箱和名字
[root@jenkins test-project]# git commit -m 'first commit'

*** Please tell me who you are.

Run

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

# 如果不确定自己是否设置,请执行下面命令进行查询
# 查看全局配置的姓名和邮箱(最常用)
git config --global user.name
git config --global user.email

# 查看当前仓库(本地)的姓名和邮箱
git config user.name
git config user.email

#设置邮箱和姓名的命令如下:
git config --global user.email "liujunwei8866@gmail.com" #引号中是自己设置的邮箱
git config --global user.name "liujunwei" #引号中是自己定义的名字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#创建文件提交到仓库中
#创建devops.md文件,文件内容是"这是提交测试文件"
$ cat devops.md
这是提交测试文件

#将本地的更新添加到暂存区
git add .

#将暂存区的内容提交到本地仓库
$ git commit -am "first commit"
[main 5be0990] first commit
1 file changed, 1 insertion(+)
create mode 100644 devops.md



#将本地仓库提交到gitlab仓库
$ git push origin main
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To 192.168.0.222:test/test-project.git
fc49f9b..5be0990 main -> main

#命令参数解释:
#origin是远端的仓库名称,如果不知道自己的远端仓库名执行git remote -v查看
#第一个main是本地要提交的分支名
#第二个main是要提交到gitlab仓库的分支名
#如果本地提交的分支名和远端gitlab的分支名一样可以这样写git push origin main

提交后检查仓库中是否存在devops.md文件:

git常用命令补充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# git提交流程如下:
工作区(你正在编辑的文件)
↓ git add .
暂存区(Staging Area)
↓ git commit -m "..."
本地仓库(Repository) ← 这里才真正产生历史版本
↓ git push
GitLab 远程仓库

# 查看远程仓库的别名
$ git remote -v
origin git@192.168.0.222:test/test-project.git (fetch)
origin git@192.168.0.222:test/test-project.git (push)


# 如果有多个分支
# 1. 切换到你要推送的 dev 分支(非常重要!)
git checkout dev # 老命令
# 或者新命令(Git 2.23+ 推荐)
git switch dev

# 2. 确认自己在 dev 分支上
git branch # 前面带 * 的就是当前分支

# 3. 提交代码(如果你有新改动)
git add .
git commit -m "feat: 在 dev 分支完成 XXX 功能"

# 4. 第一次推送 dev 分支(关键命令)
git push -u origin dev

# 5. 以后每次在 dev 分支上推送就超级简单
git push

#本地创建分支
# 创建分支并立即切换过去(最常用!)
git switch -c dev #dev是分支名称

#git push 的完整格式可以是:
git push <远程仓库名> <本地分支>:<远程分支>
#写法,实际含义(把什么推到哪里),常见场景
git push origin main #把本地 main 推送到远程 main,99% 日常使用(最推荐)
git push origin main:main #把本地 main 推送到远程 main(完全等价于上面),显式写法,清晰但啰嗦
git push origin master:main #把本地 master 推送到远程 main(名字不同),过渡期老项目重命名时
git push origin dev:develop #把本地 dev 推送到远程 develop(名字故意不同),特殊映射需求

4.Harbor安装部署

Harbor是一个开源的容器镜像仓库(Registry),由VMware公司开发并维护。它旨在提供一个企业级的解决方案,用于存储、管理和分发Docker镜像和其他容器镜像。

如果机器资源不够也可以使用阿里云的镜像仓库代替。

4.1安装docker

由于Harbor 是采用 docker-compose 一键部署的,所以 Harbor 服务器也需要安装 Docker

首先在 GitHub 下载最新的 Harbor 离线包,并上传至 Harbor 服务器,官方下载地址:https://github.com/goharbor/harbor/releases/

1
2
3
4
5
6
7
8
#安装docker环境
yum install -y yum-utils device-mapper-persistent-data lvm2

yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

yum install docker-ce-20.10.* docker-ce-cli-20.10.*

systemctl daemon-reload && systemctl enable --now docker

4.2安装Docker-Compose

安装 Docker-Compose ,下载地址:https://github.com/docker/compose/releases/,下载后把二进制 文件上传至/usr/local/bin/并添加执行权限:

1
2
3
4
5
6
7
8
9
#在线下载
curl -L "https://github.com/docker/compose/releases/download/v2.32.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose


#离线下载,如果在下加载很慢的话可以用浏览器先下载后上传到服务器/usr/local/bin/目录
[root@harbor bin]# ll
总用量 63164
-rw-r--r--. 1 root root 64678453 12月 23 16:46 docker-compose-linux-x86_64
#centos7.9系统下载的是docker-compose-linux-x86_64(在上面的下载地址中能看到),docker-compose-linux-x86_64这个文件是适用于 CentOS 7.9(一个基于 x86_64 架构的 Linux 发行版)的 Docker Compose 安装包。其他文件是为不同的操作系统或架构设计的,比如 darwin 是 macOS,arm 是 ARM 架构的设备,ppc64le 是 PowerPC 架构等。

重命名并添加执行权限即可:

1
2
3
4
5
6
7
8
9
#重命名,进入目标路径/usr/local/bin/
[root@harbor bin]# mv docker-compose-linux-x86_64 docker-compose

#添加可执行权限
[root@harbor bin]# chmod +x /usr/local/bin/docker-compose

#查看版本
[root@harbor ~]# docker-compose -v
Docker Compose version v2.32.1

4.3安装配置harbor

下载harbor离线包并上传到服务器,下载地址https://github.com/goharbor/harbor/releases/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 将下载好的安装包上传至服务器,笔记用使用的harbor板本是v2.11.2
[root@harbor ~]# ll
总用量 612608
-rw-------. 1 root root 1643 12月 23 2024 anaconda-ks.cfg
drwxr-xr-x. 3 root root 181 12月 23 2024 harbor
-rw-r--r--. 1 root root 627302753 12月 23 2024 harbor-offline-installer-v2.11.2.tgz


#将 Harbor 离线包解压并载入 Harbor 镜像:
#解压安装包
[root@harbor ~]# tar xf harbor-offline-installer-v2.11.2.tgz

#解压后会产生一个harbor目录,进入该目录
[root@harbor ~]# cd harbor

#将镜像加载本地
[root@harbor harbor]# docker load -i harbor.v2.11.2.tar.gz

#加载完成后可以看到用到的所有镜像
[root@harbor harbor]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
goharbor/harbor-exporter v2.11.2 520de0cd30c7 5 weeks ago 108MB
goharbor/redis-photon v2.11.2 bb0d92ddf3ec 5 weeks ago 165MB
goharbor/trivy-adapter-photon v2.11.2 0962772f9c8f 5 weeks ago 347MB
goharbor/harbor-registryctl v2.11.2 075c10d45191 5 weeks ago 162MB
goharbor/registry-photon v2.11.2 1365718c5208 5 weeks ago 84.8MB
goharbor/nginx-photon v2.11.2 2949037133e7 5 weeks ago 154MB
goharbor/harbor-log v2.11.2 9ae20475f5ca 5 weeks ago 163MB
goharbor/harbor-jobservice v2.11.2 8dbbe22ef281 5 weeks ago 159MB
goharbor/harbor-core v2.11.2 6c2be6bdb874 5 weeks ago 185MB
goharbor/harbor-portal v2.11.2 a3440cd04321 5 weeks ago 162MB
goharbor/harbor-db v2.11.2 a5fc5485967b 5 weeks ago 271MB
goharbor/prepare v2.11.2 74c41ed4e2a9 5 weeks ago 205MB

修改harbor配置文件,Harbor 默认提供了一个配置文件模板,根据模板文件配置一个正式文件,修改如下字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
[root@harbor harbor]# pwd
/root/harbor

[root@harbor harbor]# ll
总用量 616576
drwxr-xr-x. 3 root root 20 12月 23 2024 common
-rw-r--r--. 1 root root 3646 11月 14 2024 common.sh
-rw-r--r--. 1 root root 5913 12月 23 2024 docker-compose.yml
-rw-r--r--. 1 root root 631306450 11月 14 2024 harbor.v2.11.2.tar.gz
-rw-r--r--. 1 root root 14281 12月 23 2024 harbor.yml
-rwxr-xr-x. 1 root root 1975 11月 14 2024 install.sh
-rw-r--r--. 1 root root 11347 11月 14 2024 LICENSE
-rwxr-xr-x. 1 root root 1882 11月 14 2024 prepare

[root@harbor harbor]# cp harbor.yml harbor.yml.tmpl
[root@harbor harbor]# ll
总用量 616576
drwxr-xr-x. 3 root root 20 12月 23 2024 common
-rw-r--r--. 1 root root 3646 11月 14 2024 common.sh
-rw-r--r--. 1 root root 5913 12月 23 2024 docker-compose.yml
-rw-r--r--. 1 root root 631306450 11月 14 2024 harbor.v2.11.2.tar.gz
-rw-r--r--. 1 root root 14281 12月 23 2024 harbor.yml
-rw-r--r--. 1 root root 14270 11月 14 2024 harbor.yml.tmpl
-rwxr-xr-x. 1 root root 1975 11月 14 2024 install.sh
-rw-r--r--. 1 root root 11347 11月 14 2024 LICENSE
-rwxr-xr-x. 1 root root 1882 11月 14 2024 prepare

#由于我这是测试环境,没有正式的证书,所以我将hostname的值改为harbor服务器的ip(如果有域名和证书可以配置自己的域名)
[root@harbor harbor]# vim harbor.yml
1 # Configuration file of Harbor
2
3 # The IP address or hostname to access admin UI and registry service.
4 # DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
5 hostname: 192.168.0.89
6
7 # http related config
8 http:
9 # port for http, default is 80. If https enabled, this port will redirect to https port
10 port: 80
11
12 # https related config
13 #https:
14 # # https port for harbor, default is 443
15 # port: 443
16 # # The path of cert and key files for nginx
17 # certificate: /your/certificate/path
18 # private_key: /your/private/key/path
19 # # enable strong ssl ciphers (default: false)
20 # # strong_ssl_ciphers: false

---------------------------------------------------------------------------------------
#如自己有域名和证书可以使用域名最好,配置如下:
5 hostname: 192.168.0.89 #这里hostname的ip改为自己的域名
6
7 # http related config
8 http:
9 # port for http, default is 80. If https enabled, this port will redirect to https port
10 port: 80
11
12 # https related config
13 https: #这里的https打开,由于我没有域名所以上面我给注释了
14 # https port for harbor, default is 443
15 port: 443
16 # The path of cert and key files for nginx
17 certificate: /your/certificate/path #这里配置证书路径
18 private_key: /your/private/key/path #这里配置证书key路径
19 # enable strong ssl ciphers (default: false)
20 # strong_ssl_ciphers: false
47 harbor_admin_password: Harbor12345 #该字段是定义harbor的登录密码,配置仅在 Harbor 首次初始化安装时生效,运行过的harbor修改密码需要在web控制台进行修改
65 # The default data volume
66 data_volume: /data/harbor #这里我将harbor的数据目录修改为/data/harbor

1.hostname:是配置Harbor 的访问地址,可以是域名或者 IP,生产推荐使用域名,并且带有购买的证书;
2.https:域名证书的配置,生产环境需要配置权威证书供 Harbor 使用,否则需要添加insecure-registry 配置来解决无法推送镜像的问题,由于我这里是测试并没有配置证书,所以需要配置insecure-registry,配置方法会在下面笔记中记录
4.账号密码按需修改即可,默认账号是admin,密码是默认为Harbor12345
---------------------------------------------------------------------------------------

#创建harbor的数据目录
[root@harbor harbor]# mkdir -p /data/harbor

#修改harbor的数据目录
[root@harbor harbor]# vim harbor.yml
#大概在文件的66行原值是/data,我这里改为了/data/harbor
65 # The default data volume
66 data_volume: /data/harbor

上述修改后保存并进行预配置检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@harbor harbor]# ./prepare  #每次修改了harbor.yml都要执行
prepare base dir is set to /root/harbor
WARNING:root:WARNING: HTTP protocol is insecure. Harbor will deprecate http protocol in the future. Please make sure to upgrade to https
Generated configuration file: /config/portal/nginx.conf
Generated configuration file: /config/log/logrotate.conf
Generated configuration file: /config/log/rsyslog_docker.conf
Generated configuration file: /config/nginx/nginx.conf
Generated configuration file: /config/core/env
Generated configuration file: /config/core/app.conf
Generated configuration file: /config/registry/config.yml
Generated configuration file: /config/registryctl/env
Generated configuration file: /config/registryctl/config.yml
Generated configuration file: /config/db/env
Generated configuration file: /config/jobservice/env
Generated configuration file: /config/jobservice/config.yml
Generated and saved secret to file: /data/secret/keys/secretkey
Successfully called func: create_root_cert
Generated configuration file: /compose_location/docker-compose.yml
Clean up the input dir

执行安装命令:

1
2
3
[root@harbor harbor]# ./install.sh
#看到下面信息表示harbor成功安装并启动了
✔ ----Harbor has been installed and started successfully.----

成功启动后,即可通过配置的地址或域名访问:

用户名:admin

密码:Harbor12345

如果机器重启后遇到harbor无法启动,需要手动启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 启动命令,进入到harbor的安装目录/root/harbor(离线包节后生成的那个目录)
[root@harbor harbor]# docker-compose up -d

#启动后查看容器状态,都是healthy
[root@harbor harbor]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
43cb7034559c goharbor/nginx-photon:v2.11.2 "nginx -g 'daemon of…" About a minute ago Up About a minute (healthy) 0.0.0.0:80->8080/tcp, :::80->8080/tcp nginx
0909b50d18e9 goharbor/harbor-jobservice:v2.11.2 "/harbor/entrypoint.…" About a minute ago Up About a minute (healthy) harbor-jobservice
acf65dc41a89 goharbor/harbor-core:v2.11.2 "/harbor/entrypoint.…" About a minute ago Up About a minute (healthy) harbor-core
5711d91ff05d goharbor/harbor-db:v2.11.2 "/docker-entrypoint.…" About a minute ago Up About a minute (healthy) harbor-db
c294bf9984cf goharbor/harbor-registryctl:v2.11.2 "/home/harbor/start.…" About a minute ago Up About a minute (healthy) registryctl
b40ef848fc7c goharbor/harbor-portal:v2.11.2 "nginx -g 'daemon of…" About a minute ago Up About a minute (healthy) harbor-portal
05c0a1dc14f4 goharbor/redis-photon:v2.11.2 "redis-server /etc/r…" About a minute ago Up About a minute (healthy) redis
61767eb7b912 goharbor/registry-photon:v2.11.2 "/home/harbor/entryp…" About a minute ago Up About a minute (healthy) registry
cb308be80884 goharbor/harbor-log:v2.11.2 "/bin/sh -c /usr/loc…" About a minute ago Up About a minute (healthy) 127.0.0.1:1514->10514/tcp harbor-log


# 停止harbor服务
[root@harbor harbor]# docker-compose down
1
2
3
4
#  如果需要修改 harbor.yml 先停止服务 docker-compose down 在修改文件
# 修改后必须 运行 ./prepare
# 在启动harbor服务 docker-compose up -d
# 不需要 强制运行 ./install.sh(除非官方文档针对特定版本有特殊说明)。

4.4docker配置insecure-registries

如果 Harbor 配置不是权威的 https 协议,所有的 Docker 节点(如果是 containerd 作为 Runtime, 参考下文配置 insecure-registry)都需要添加 insecure-registries 配置,由于我使用docker制作镜像,推拉镜像,而我的k8s的Runtime又使用的是containerd ,所以这里我都需要修改,步骤如下:

只要docker所在的节点需要推送镜像到harbor仓库,这个节点上的docker都需要配置insecure-registries

1
2
3
4
5
6
7
8
9
10
11
12
#注意:修改的是所有k8s节点的docker
#配置docker 的insecure-registries 参数,daemon.json文件如果不存在就新创建该文件:
[root@k8s-master01 ~]# vim /etc/docker/daemon.json
#添加以下内容"insecure-registries": ["192.168.0.89"] 参数
{
"exec-opts": ["native.cgroupdriver=systemd"],
"insecure-registries": ["192.168.0.89"]
}

#保存退出后重启docker
systemctl daemon-reload
systemctl restart docker

推送镜像到harbor仓库测试服务是否正常:

先在harbor创建一个名为kubernetes的项目:

之后在服务器上找任意一个镜像,修改 tag 为 Harbor 的地址,并进行 Push 测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 先登录harbor,Login Succeeded表示正常
[root@harbor harbor]# docker login 192.168.0.89
Username: admin
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded


# 本地有的镜像
[root@harbor harbor]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
goharbor/harbor-exporter v2.11.2 520de0cd30c7 16 months ago 108MB
goharbor/redis-photon v2.11.2 bb0d92ddf3ec 16 months ago 165MB
goharbor/trivy-adapter-photon v2.11.2 0962772f9c8f 16 months ago 347MB
goharbor/harbor-registryctl v2.11.2 075c10d45191 16 months ago 162MB
goharbor/registry-photon v2.11.2 1365718c5208 16 months ago 84.8MB
goharbor/nginx-photon v2.11.2 2949037133e7 16 months ago 154MB
goharbor/harbor-log v2.11.2 9ae20475f5ca 16 months ago 163MB
goharbor/harbor-jobservice v2.11.2 8dbbe22ef281 16 months ago 159MB
goharbor/harbor-core v2.11.2 6c2be6bdb874 16 months ago 185MB
goharbor/harbor-portal v2.11.2 a3440cd04321 16 months ago 162MB
goharbor/harbor-db v2.11.2 a5fc5485967b 16 months ago 271MB
goharbor/prepare v2.11.2 74c41ed4e2a9 16 months ago 205MB


#例如将goharbor/harbor-exporter:v2.11.2镜像推送到harbor
#先给镜像打tag
[root@harbor harbor]# docker tag goharbor/harbor-exporter:v2.11.2 192.168.0.89/kubernetes/harbor-exporter:v1

#查看刚才打好tag的镜像
[root@harbor harbor]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.0.89/kubernetes/harbor-exporter v1 520de0cd30c7 16 months ago 108MB
goharbor/harbor-exporter v2.11.2 520de0cd30c7 16 months ago 108MB
goharbor/redis-photon v2.11.2 bb0d92ddf3ec 16 months ago 165MB
goharbor/trivy-adapter-photon v2.11.2 0962772f9c8f 16 months ago 347MB
goharbor/harbor-registryctl v2.11.2 075c10d45191 16 months ago 162MB
goharbor/registry-photon v2.11.2 1365718c5208 16 months ago 84.8MB
goharbor/nginx-photon v2.11.2 2949037133e7 16 months ago 154MB
goharbor/harbor-log v2.11.2 9ae20475f5ca 16 months ago 163MB
goharbor/harbor-jobservice v2.11.2 8dbbe22ef281 16 months ago 159MB
goharbor/harbor-core v2.11.2 6c2be6bdb874 16 months ago 185MB
goharbor/harbor-portal v2.11.2 a3440cd04321 16 months ago 162MB
goharbor/harbor-db v2.11.2 a5fc5485967b 16 months ago 271MB
goharbor/prepare v2.11.2 74c41ed4e2a9 16 months ago 205MB

#将192.168.0.89/kubernetes/harbor-exporter:v1 镜像推送到 harbor
[root@harbor harbor]# docker push 192.168.0.89/kubernetes/harbor-exporter:v1
The push refers to repository [192.168.0.89/kubernetes/harbor-exporter]
77dcdafb6660: Pushed
3c22ae1a8288: Pushed
affe8930250d: Pushed
6d23bb381515: Pushed
7e3e085aad00: Pushed
v1: digest: sha256:9daeec50c2e00820059f513c93539dd29ee13554180477825d8799daf93738aa size: 1371

推送成功检查harbor镜像仓库中是否存在该镜像:

4.5containerd配置insecure-registries

如果 Kubernetes 集群采用的是 Containerd 作为的 Runtime,那么 Containerd 也需要配置 insecure registry。

1
2
3
4
5
6
7
8
9
10
11
# 首先生成 Containerd 的配置(如果已经生成过,请勿执行),注意:是所有k8s节点的containerd都需要修改
[root@k8s-master01 ~]# ll /etc/containerd/
total 12
-rw-r--r-- 1 root root 8794 Apr 21 2025 config.toml

#接下来在配置文件中添加 mirrors 配置,修改配置文件
[root@k8s-master01 ~]# vim /etc/containerd/config.toml
#添加171和172行,注意格式,参考下图
170 [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
171 [plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.0.89"]
172 endpoint = ["http://192.168.0.89"]

配置完成后,重启 Containerd,测试镜像推送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 重启containerd服务
[root@k8s-master01 ~]# systemctl restart containerd

#containerd推送镜像命令
ctr images push --plain-http --user=admin:Harbor12345 192.168.0.89/kubernetes/nginx:latest

#containerd查看本地存在的镜像
[root@k8s-master01 ~]# ctr images list
WARN[0000] DEPRECATION: The `mirrors` property of `[plugins."io.containerd.grpc.v1.cri".registry]` is deprecated since containerd v1.5 and will be removed in containerd v2.1. Use `config_path` instead.
REF TYPE DIGEST SIZE PLATFORMS LABELS
192.168.0.89/kubernetes/nginx:latest application/vnd.docker.distribution.manifest.v2+json sha256:9d1cac272a1ff8c4f4d6607940f179c9716b28c956231e61ae40a63990400c15 67.7 MiB linux/amd64 -

#注意containerd中也有命名空间概念
ctr -n k8s.io i ls 是查看k8s.io命名孔家中有哪些镜像,i是images的缩写, ls是list的缩写
ctr images ls 是查看default命名空间中有哪些镜像


#containerd拉取镜像命令
ctr images pull --plain-http --user=admin:Harbor12345 192.168.0.89/kubernetes/nginx:latest

检查仓库中是否存在推送的192.168.0.89/kubernetes/nginx:latest镜像


到此Harbor配置结束!!!接下来需要做一些配置,把各个工具整合到 Jenkins。

5.Jenkins凭证管理

Harbor 的账号密码、GitLab 的私钥、Kubernetes 的证书均使用 Jenkins 的 Credentials 管理,通过Credentials 的配置在Jenkins集成各个工具。

5.1配置kubernetes证书

首先需要找到集群中的 KUBECONFIG,一般是 kubectl 节点的~/.kube/config 文件,或者是KUBECONFIG 环境变量所指向的文件。

我的是二进制安装的集群,所以文件是/etc/kubernetes/admin.kubeconfig

将/etc/kubernetes/admin.kubeconfig文件下在到本地电脑,然后上传到jenkins控制台中,配置如下:

接下来只需要把证书文件放置于 Jenkins 的 Credentials 中即可。首先点击 Manage Jenkins,在点击Credentials:

点击global旁边的倒三角,会出现Add credentials,点击添加一个凭证 Secret file 类型的凭证:

按照下图操作配置k8s集群的凭证:

凭证类型:Secret file

File:选择上述步骤中下载到本地电脑的admin.kubeconfig文件

ID:该凭证的id,在流水线中会引用这个id名字

Description:对这个凭证的描述,知道这个凭证是用来干什么的

注意:如果有多个k8s集群环境,ID不能重复

点击Create创建后如图所示:

5.3配置Harbor账号密码

还是找到Add credentials按钮,添加一个Username with password类型的凭证:

  • Username:登录Harbor 平台的用户名;
  • Password:登录Harbor 平台的密码;
  • ID:该凭证的 ID;
  • Description:给证书添加的描述。

创建成功后如下图:

5.4配置gitlab key

Jenkins要实现从gitlab免密拉取代码,要在Jenkins机器上生成密钥对,把公钥配置到gitlab平台,同时也要把私钥配置到jenkins中才行,具体参考下面步骤:

步骤一:先在Jenkins的服务器上生成密钥对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 首先在Jenkins的服务器上生成密钥对
[root@jenkins ~]# ssh-keygen -t rsa -C "jenkins@192.168.0.87"
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:pCp/mH6B/NWSuer1L10of2XJzsiWM/b5jyG+8FXP2Kg jenkins@192.168.0.87
The key's randomart image is:
+---[RSA 3072]----+
| |
| |
| . |
| o |
| . .. S+ .. o|
| o.. = o . .O+|
| . .+ o.o.+o.Xo+|
| oo +....+o%.=.|
| .o+o. .oEo=o=|
+----[SHA256]-----+


# 检查生成的两个文件
[root@jenkins ~]# ll /root/.ssh/
总用量 8
-rw------- 1 root root 2610 3月 23 23:32 id_rsa #私钥
-rw-r--r-- 1 root root 574 3月 23 23:32 id_rsa.pub #公钥

步骤二:把公钥的内容添加到gitlab的项目中:

在gitlab的管理界面User Settings → SSH Keys配置公钥是可以的,但不推荐用于 Jenkins CI/CD(权限过大,泄露风险高)。

推荐做法:用 Deploy Keys(项目设置里),而不是用户 SSH Keys,参考如下:

test-project项目为例:点击进入该项目-[设置]-[仓库]-[部署密钥]-[添加新密钥]

这里添加的是密钥会在[私有访问的部署密钥]中显示,这里的密钥可以在别的项目中复用。

1
2
[root@jenkins ~]# cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCzUWuQeXUBaEMKL6b27gifym08gHj1hpRjbnE1AnkqHBBYzJcE88EXiFpJ3HpbxwPNL2zqSHn0evK8OGmIrzjP6qyA/nVQaPiwPa06XDhM1G6eFSMts+0LGXSM5ljxPg3NHUhFb48RnByi6QcOTfj9sm1BTmyqABIlfQDGqccNmQbpITN+yM8oeBreci6JRPb9MbeLJt7b6VIHIsYFzvh6GNMruwVoo2b/XhXYEKgx29bjS3igUZQEeHT167t5qePxH1jDgUaGUu1TQ5q1iFA7LZ21wYmWQh6MFQ/+JhMXK+Lz1ZEoV1kuef/S6XW/cMwXDvuMMi1CQ4wXs899EaBRGxrcyQZWH7uNF73MVOI5px8FYG9d0MnW2Y6/M8A3G9xa+o2fWvD39KbvBUGqSLrs7zXMmPJA+4TXAC3oBPc68zbAjZFmDDM4Ov6ojNJon884YRnixPSujre+VxdEIEhazAVHFnfDPiNFTpyOEUPgLyOd88F/4Nq0szaZ0AtbRdM= jenkins@192.168.0.87


如果需要在其他项目复用这个公钥可以点击[启用]即可,可以避免手动为每个项目添加。

[公开访问的部署密钥]是在[][][管理中心]-[部署密钥]-[新建部署密钥]中添加的


步骤三:在Jenkins中配置credentials管理私钥

还是点击 Add Credentials添加一个凭证,类型选择为 SSH Username with private key,下图中Username中的gitlab是错误的,应该使用git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[root@jenkins ~]# cat .ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAs1FrkHl1AWhDCi+m9u4In8ptPIB49YaUY25xNQJ5KhwQWMyXBPPB
F4haSdx6W8cDzS9s6kh59HryvDhpiK84z+qsgP51UGj4sD2tOlw4TNRunhUjLbPtCxl0jO
ZY8T4NzR1IRW+PEZwcoukHDk34/bJtQU5sqgASJX0AxqnHDZkG6SEzfsjPKHga3nIuiUT2
/TG3iybe2+lSByLGBc74ehjTK7sFaKNm/14V2BCoMdvW40t4oFGUBHh09eu7eanj8R9Yw4
FGhlLtU0OatYhQOy2dtcGJlkIejBUP/iYTFyvi89WRKFdZLnn/0ul1v3DMFw77jDItQkOM
F7PPfRGgURsa3MkGVh+7jRe9zFTiOacfBWBvXdDJ1tmOvzPANxvcWvqNn1rw9/Sm7wVBqk
i67O81zJjyQPuE1wAt6AT3OvM2wI2RZgwzODr+qIzSaJ/POGEZ4sT0ro63vlcXRCBIWswF
RxZ3wz4jRU6cjhFD4C8jnfPBf+DatLM2mdALW0XTAAAFkKwOb96sDm/eAAAAB3NzaC1yc2
EAAAGBALNRa5B5dQFoQwovpvbuCJ/KbTyAePWGlGNucTUCeSocEFjMlwTzwReIWkncelvH
A80vbOpIefR68rw4aYivOM/qrID+dVBo+LA9rTpcOEzUbp4VIy2z7QsZdIzmWPE+Dc0dSE
VvjxGcHKLpBw5N+P2ybUFObKoAEiV9AMapxw2ZBukhM37Izyh4Gt5yLolE9v0xt4sm3tvp
UgcixgXO+HoY0yu7BWijZv9eFdgQqDHb1uNLeKBRlAR4dPXru3mp4/EfWMOBRoZS7VNDmr
WIUDstnbXBiZZCHowVD/4mExcr4vPVkShXWS55/9Lpdb9wzBcO+4wyLUJDjBezz30RoFEb
GtzJBlYfu40XvcxU4jmnHwVgb13QydbZjr8zwDcb3Fr6jZ9a8Pf0pu8FQapIuuzvNcyY8k
D7hNcALegE9zrzNsCNkWYMMzg6/qiM0mifzzhhGeLE9K6Ot75XF0QgSFrMBUcWd8M+I0VO
nI4RQ+AvI53zwX/g2rSzNpnQC1tF0wAAAAMBAAEAAAGAG3W4dyNiW5jPy1uIwf+pqm6hU8
aIv3CggKmFBI0gaOZrSm3qIoQBac0jpnPw+OT9HArLLwVyR5IcYd5xqtDKbmyNhiNxa6Ln
knWfdXRx2rIdTnAGNxV6k1yOkJUlwauzCVoWmyt+SUAS0o/E2Nfv7UnJmVqdIVtpa/w8hi
+xn89zU3gWHrA5qQTbeH0by0GYYSWf6dtF/X85ZIZatMEBJBFxJR06ss9zEmAiEDhcyWXU
/nGsQ0C63gRkRNSYnQT69VswD9SN+tSzBYLKdhqMIFAA0MYOid4ptEhCeWFvow0kSrjaHp
0oGEfa3goYrbT2sIzpG1jdHIISCH78WKZswiYZcRCp5+m+CC3QL02kFNxbOYSlqwLTRtk8
yfxJygQ4HNSY3kaDeSQCfnakH/bc7SnAnJarj4316pEnIZGXZJhBgn4FalLUX+eE/0FhSB
xThNqhJvu+tkkM1pA9e3Md9A8XIlenD9aqAPBagZ8KEhorh3yzMxRdGZKvfdT1J6XBAAAA
wQC5QEiS0HY7dbsGC8Rg1nIdJA5lKVu3vNE5JTNegCB3aXNdxPXmbByyw77+899xcwX5RJ
4RisXbczd7iyj2H0i4FZuqCOH88p74vkW5uKysndSVSeqHu1q8gxIlxlpYydVnveynVHjP
MrIo9FfXjFHVmXOWkJn7nsBqZlfYZTekcIQVPsZfqUzcHs7Esu1S0a4z/lQ+8C7QyMgaja
wggd1yxFbTPDoy1AhpUqj6NFgZiUwjyYEGmdts+KA4u8VJMGcAAADBAODeWP/Lhma1qBdw
H1uCnHl7E8tGv6Nn9Zg+x7/9VZOLK8Galf6gJkMe7Ab7FRTZfg+uziR6CZ1LSn4zZARYiE
6k4UBYTRBDZJM/V5gHqlwfBvsQj/WEnPNIb1hFzCivdQTb4u4hPuJh7ufImDC0wwsDIwNL
mrSCm1AwBsLoiDena/ovScCOD6WLjOZKEM/TSfXs03W3+MhzxJ69TdLmtnE8V2A83EGRml
6p/bDnkPwZdYh1gV9MbIE01dLjnG0qawAAAMEAzCSzKboXeqlDVzoGgFxfgbg5SlEtVJTk
BJ9RfGbADXQ/AVPbCUmmE90w0915DqSpNRi2U5GPJuDTcN3cOhs2mtNkLhbb8wTmLktHXK
j2F63YBJn4G6iPAG3UHyxNEnQalL/v5jIz4O/8bdpWqPJthojaM1UXuLc3d1PvkdwGyCBF
wZWvm+U5pPOuZcIubX78MxgwwQAvyB0dX/SyZAwhL+/qrTuF43qyId0pK3wMCdSkwbfutf
d/58p3noHVw3w5AAAAFGplbmtpbnNAMTkyLjE2OC4wLjg3AQIDBAUG
-----END OPENSSH PRIVATE KEY-----

配置项 建议输入内容 深度解析
Kind (类型) 保持默认 SSH Username with private key 是专门用于 SSH 密钥对认证的类型。
Scope (范围) Global 保持全局范围,这样你所有的 Pipeline 项目都能引用这个凭据。
ID gitlab-ssh-key 核心项。这是你在 Jenkinsfile 中通过 credentials('ID') 引用的唯一标识。建议起一个能体现用途的名字。
Description GitLab 拉取代码专用密钥 方便你在凭据列表中快速识别。
Username **git**截图中的gitlab是错误的 非常重要! 对于 GitLab 的 SSH 访问,底层连接用户始终是 git。即使你是个人的 GitLab 账号,这里也请填写 git 而不是你的邮箱或姓名。
Private Key 选择 “Enter directly” 点击 Add 按钮,将你在服务器上生成的 id_rsa(私钥) 内容完整粘贴进去。必须包含 -----BEGIN OPENSSH PRIVATE KEY----------END... 这两行。
Passphrase 留空 如果你之前用 ssh-keygen 生成密钥时没有设置密码(直接一路回车),这里就保持为空。如果设置了,必须填入对应的密码。

注意:Private Key中添加的是步骤一中在jenkins服务器上生成的私钥(/root/.ssh/id_rsa),在步骤二中我已经把公钥添加到gitlba中了,这里的私钥要和添加的公钥配对才可以。

6.配置Agent端口

在 Jenkins 集成 Kubernetes (K8S) 实现 CI/CD 的架构中,50000 端口(默认值)扮演着核心的通信角色。简单来说,它是 Jenkins Controller(主节点)用来接收 Agent(代理节点)连接的“接线员”端口

Jenkins 在 K8S 中通常采用 动态 Agent 模式:每当有流水线任务时,Controller 会在 K8S 集群中动态创建一个 Pod 作为 Agent。

  • 通信方向:Agent Pod 启动后,需要“主动”联系 Controller 领取任务。
  • 协议:这种主动连接通常使用 **JNLP (Java Network Launch Protocol)**,现在的版本通常称为 Inbound TCP Agent 协议。
  • 端口作用:50000 端口就是 Controller 监听这些 TCP 连接的专用端口。

点击 Manage Jenkins,然后点击 Security ,找到Agents模块,配置5000端口,参考下图步骤:

实际使用时,没有必要把整个 Kubernetes 集群的节点都充当创建 Jenkins Slave Pod 的节点,可以选择任意的一个或多个节点作为创建 Slave Pod 的节点。

这里我用k8s-node01 节点作为slave节点,为该节点打个标签,后续在pipeline 中根据标签选择这个节点作为slave:

1
2
[root@k8s-master01 ~]# kubectl label nodes k8s-node01 build=true
node/k8s-node01 labeled

如果集群并非使用 Docker 作为 Runtime,但是由于构建镜像时,需要使用 Docker,所以该节点需要安装 Docker(如果没有固定某个节点作为slave那么k8s所有节点都需要安装docker):

1
2
3
4
5
6
7
8
9
#如果节点上没有安装docker按照下面步骤进行安装,如果已经安装了忽略该步骤
#安装docker环境
yum install -y yum-utils device-mapper-persistent-data lvm2

yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

yum install docker-ce-20.10.* docker-ce-cli-20.10.*

systemctl daemon-reload && systemctl enable --now docker

如果镜像仓库未配置证书,需要配置 insecure-registry(参考Harbor安装中的4.4小节):

1
2
3
4
5
6
vim /etc/docker/daemon.json
#添加"insecure-registries"参数
{
"exec-opts": ["native.cgroupdriver=systemd"],
"insecure-registries": ["192.168.0.89"] #这里的ip是harbor仓库的地址
}

7.Jenkins连接k8s计群

参考下图操作,在Jenkins中添加一个k8s集群,点击 [Manage Jenkins[,再点击 [Clouds[] ,点击[New cloud[添加一个k8s集群:

最后点击 Save 即可,添加完 Kubernetes 后,在 Jenkinsfile 的 Agent 中,就可以选择该集群作为创建 Slave 的集群。

如果想要添加多个集群,重复上述的步骤即可。首先添加 Kubernetes 凭证,然后添加 Cloud即可。

8.自动化流水线设计

8.1自动化流水线模板

针对如上的逻辑,可以设计一个流水线模板,只需要变更一些参数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false)
yaml '''
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never
securityContext:
runAsUser: 0
fsGroup: 0
# nodeSelector:
# build: "true"
containers:
# jnlp容器,负责和Jenkins主节点通信,该容器中的配置不需要修改
- name: jnlp
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jnlp-agent-docker:latest
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
imagePullPolicy: IfNotPresent
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
# build容器,包含执行构建的命令,比如java的需要mvn构建,就可以用一个maven的镜像
- name: build #定义容器的名字,流水线的stage会用到改名字
# java程序使用maven镜像,包含mvn工具。如果是NodeJS项目可以使用node的镜像,根据实际情况修改
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/maven:3.5.3
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: maven-cache
mountPath: /root/.m2
readOnly: false
- name: localtime
mountPath: /etc/localtime
# kaniko 用来制作镜像的容器,可以是docker 或者其他可以用于构建镜像的工具
- name: kaniko
# 是示例中采用kaniko制作镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kaniko-executor:debug
imagePullPolicy: IfNotPresent
command:
- "sleep"
args:
- "99d"
tty: true
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
- name: docker-registry-config #挂载docker配置文件,用于kaniko连接镜像仓库
mountPath: /kaniko/.docker
readOnly: false
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 定义发版的镜像,因为最终是发版至 Kubernetes集群,所以需要有一个 kubectl 命令或者是其他的发版工具也可以,比如 helm,根据实际情况配置自己的发版镜像
- name: kubectl
# 发版的镜像根据实际需求进行变更,例如使用helm的镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kubectl:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
# 构建缓存的pvc
- name: maven-cache
persistentVolumeClaim:
claimName: maven-global-cache
readOnly: false
#docker认证信息的volume
- name: docker-registry-config
configMap:
name: docker-registry-config '''
}
}

//定义一些全局环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.0.89" //Harbor 地址
REGISTRY_DIR = "kubernetes" //Harbor 的项目目录
IMAGE_NAME = "spring-boot-project" //镜像的名称
NAMESPACE = "kubernetes" //该应用在 Kubernetes 中的命名空间
TAG = "" //镜像的 Tag,在此用 BUILD_TAG+COMMIT_ID 组成
GIT_URL = "git@192.168.0.222:kubernetes/spring-boot-project.git" //代码仓库地址
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue:'', description: 'Branch for build and deploy', name: 'BRANCH',quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',tagFilter: '*', type: 'PT_BRANCH')
}
// 接下来是拉代码的 stage,这个 stage 是一个并行的 stage,因为考虑了该流水线是手动触发还是触发
stages {
stage('拉取代码') {
steps {
script {
def branchToUse = env.gitlabBranch ?: params.BRANCH
// 加一个保护,防止两个来源都为空
if (!branchToUse) {
error "未指定构建分支,请检查触发方式或参数配置"
}
git(
url: "${GIT_URL}",
branch: branchToUse,
changelog: true,
poll: true,
credentialsId: 'gitlab-key'
)
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = "${env.BUILD_TAG}-${COMMIT_ID}"
echo "触发方式: ${env.gitlabBranch ? 'GitLab Webhook' : '手动触发'}"
echo "Branch: ${branchToUse}, Commit: ${COMMIT_ID}, Tag: ${TAG}"
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh """
mvn clean install -DskipTests
ls target/*
"""
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('部署到k8s') {
environment {
MY_KUBECONFIG = credentials('STUD_KUBECONFIG')
}
steps {
container(name: 'kubectl') {
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
}

上述模板是把Jenkins的工作目录和maven的缓存目录分开了,分别使用不同的pvc进行持久化,如果项目小也可以使用一个pvc进行持久化,下面是用一个pvc的配置案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false) //这里插件自动注入的 volume 名字是 workspace-volume,直接在build容器中使用即可,不在需要单独定义卷的名字
yaml '''
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never
securityContext:
runAsUser: 0
fsGroup: 0
# nodeSelector:
# build: "true"
containers:
# build容器,包含执行构建的命令,比如java的需要mvn构建,就可以用一个maven的镜像
- name: build #定义容器的名字,流水线的stage会用到改名字
# java程序使用maven镜像,包含mvn工具。如果是NodeJS项目可以使用node的镜像,根据实际情况修改
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/maven:3.5.3
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
volumeMounts:
- name: workspace-volume # 这里使用插件自动注入的卷名
mountPath: /root/.m2
readOnly: false
- name: localtime
mountPath: /etc/localtime
volumes: #在volumes中不在需要单独定义maven持久化使用的卷
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
#docker认证信息的volume
- name: docker-registry-config
configMap:
name: docker-registry-config '''
}
}
}
#其他省略的保持不变即可。

8.2模板详解

首先定义顶层的Agent,定义的是kubernetesd的pod作为Jenkins的Slave:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
    agent {   // 定义流水线在哪个代理节点 (agent) 上运行
kubernetes { // 指定使用 Kubernetes 作为动态构建代理
cloud 'kubernetes-study' //指定在Jenkins 系统配置中添加的 Kubernetes Cloud 的名称
slaveConnectTimeout 1200 // 设置 Jenkins Agent (Slave) 连接回主节点 (Master) 的超时时间为 1200 秒
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false) // 将名为 pipeline-cache 的 PVC 挂载为该任务的工作空间,允许读写,用于跨阶段缓存共享
yaml ''' // 定义 Pod 的 YAML 模板,此模板将用于动态创建包含多个工具容器的执行 Pod
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never #策略设置为Never,意味着 Pod 执行完流水线任务后不会自动重启,由 Jenkins 清理
securityContext: #如果不定义该配置,容器会没有权限将拉取的代码写入workspace中
runAsUser: 0 #强制容器主进程以 root 用户(UID 0)运行
fsGroup: 0 #kubernetes 会自动修改已挂载 Volume 中所有文件/目录的组所有者为 0(root group)
nodeSelector: #节点选择器,pod会在具有build: "true"标签的节点上创建,要提前给节点打上标签
build: "true"
containers: #定义pod中包含的容器
# jnlp容器,负责和Jenkins主节点通信,该容器中的配置不需要修改
- name: jnlp
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jnlp-agent-docker:latest
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
imagePullPolicy: IfNotPresent
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
# build容器,包含执行构建的命令,比如java的需要mvn构建,就可以用一个maven的镜像
- name: build #定义容器的名字,流水线的stage会引用该名字
# java程序使用maven镜像,包含mvn工具。如果是NodeJS项目可以使用node的镜像,根据实际情况修改
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/maven:3.5.3
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: maven-cache
mountPath: /root/.m2 //java编译时会把依赖的插件缓存到/root/.m2,所以把该目录也进行缓存
readOnly: false
- name: localtime
mountPath: /etc/localtime
# kaniko 用来制作镜像的容器,可以是docker 或者其他可以用于构建镜像的工具
- name: kaniko
# 是示例中采用kaniko制作镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kaniko-executor:debug
imagePullPolicy: IfNotPresent
command:
- "sleep"
args:
- "99d"
tty: true
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
- name: docker-registry-config #挂载docker配置文件,用于kaniko连接镜像仓库
mountPath: /kaniko/.docker
readOnly: false
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 定义发版的镜像,因为最终是发版至 Kubernetes集群,所以需要有一个 kubectl 命令或者是其他的发版工具也可以,比如 helm,根据实际情况配置自己的发版镜像
- name: kubectl
# 发版的镜像根据实际需求进行变更,例如使用helm的镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kubectl:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
# 构建缓存的pvc
- name: maven-cache
persistentVolumeClaim:
claimName: maven-global-cache //
readOnly: false
#docker认证信息的volume
- name: docker-registry-config
configMap:
name: docker-registry-config '''
}
}

接下来解读 Jenkinsfile 最后的环境变量和 parameters 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义一些全局环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.0.89" //定义Harbor 地址
REGISTRY_DIR = "kubernetes" //Harbor 的项目目录
IMAGE_NAME = "spring-boot-project" //镜像的名称
NAMESPACE = "kubernetes" //该应用在 Kubernetes 中的命名空间
TAG = "" //镜像的 Tag,在此用 BUILD_TAG+COMMIT_ID 组成
GIT_URL = "git@192.168.0.222:kubernetes/spring-boot-project.git" //代码仓库地址
}
parameters {
# 之前讲过一些 choice、input 类型的参数,本次使用的是 GitParameter 插件
# 该字段会在 Jenkins 页面生成一个选择分支的选项
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue:'', description: 'Branch for build and deploy', name: 'BRANCH',quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',tagFilter: '*', type: 'PT_BRANCH')
}

接下来是拉代码的 stage,这里考虑了该流水线是手动触发还是webhook自动触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接下来是拉代码的 stage,考虑了该流水线是手动触发还是webhook触发
stage('拉取代码') {
steps {
script {
def branchToUse = env.gitlabBranch ?: params.BRANCH
// 加一个保护,防止两个来源都为空
if (!branchToUse) {
error "未指定构建分支,请检查触发方式或参数配置"
}
git(
url: "${GIT_URL}",
branch: branchToUse,
changelog: true,
poll: true,
credentialsId: 'gitlab-key'
)
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = "${env.BUILD_TAG}-${COMMIT_ID}"
echo "触发方式: ${env.gitlabBranch ? 'GitLab Webhook' : '手动触发'}"
echo "Branch: ${branchToUse}, Commit: ${COMMIT_ID}, Tag: ${TAG}"
}
}
}

代码拉下来后,就可以执行构建命令,由于本次实验是 Java 示例,所以需要使用 mvn 命令 进行构建:

1
2
3
4
5
6
7
8
9
10
11
stage('Building') {
steps {
container(name: 'build') {
sh """
mvn clean install -DskipTests
ls target/*
"""
}
}
}

生成编译产物后,需要根据该产物生成对应的镜像,此时可以使用 Pod 模板的 kaniko 容器:

1
2
3
4
5
6
7
8
9
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}

最后一步就是将该镜像发版至 Kubernetes 集群中,此时使用的是包含 kubectl 命令的容器:

1
2
3
4
5
6
7
8
9
10
11
12
stage('部署到k8s') {
environment {
MY_KUBECONFIG = credentials('STUD_KUBECONFIG')
}
steps {
container(name: 'kubectl') {
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}

8.3准备持久化存储

Jenkins 在构建时,会产生一些依赖文件,这些文件最好进行持久化存储,防止重复下载。 接下来创建一个 PVC用于流水线工作目录及依赖文件的数据持久化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建pvc,这里我使用的是动态存储
[root@k8s-master01 36-cicd]# vim pipeline-cache-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pipeline-cache
# 【重要提醒】:这里必须填写你的 Jenkins 动态 Agent Pod 所在的命名空间
# 如果你的 Jenkins 运行在 'jenkins' 命名空间,请改为 'jenkins'
namespace: default
spec:
volumeMode: Filesystem #卷模式要用文件系统,不指定默认是文件系统。 不能使用块存储
accessModes:
# CubeFS 支持多点读写,这对于并发构建非常重要,能防止卷被独占锁死
- ReadWriteMany
storageClassName: cfs-sc
resources:
requests:
# 分配 20G 给 Maven 本地仓库通常比较充裕,可根据实际团队规模调整
storage: 20Gi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# maven持久化的pvc配置如下:
[root@k8s-master01 36-cicd]# cat maven-cache.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: maven-global-cache
# 【重要提醒】:这里必须填写你的 Jenkins 动态 Agent Pod 所在的命名空间
namespace: default
spec:
volumeMode: Filesystem #卷模式要用文件系统,不指定默认是文件系统。 不能使用块存储
accessModes:
# CubeFS 支持多点读写,这对于并发构建非常重要,能防止卷被独占锁死
- ReadWriteMany
storageClassName: cfs-sc
resources:
requests:
# 分配 20G 给 Maven 本地仓库通常比较充裕,可根据实际团队规模调整
storage: 20Gi

创建pvc资源:

1
2
3
4
5
[root@k8s-master01 36-cicd]# kubectl create -f pipeline-cache-pvc.yaml
persistentvolumeclaim/pipeline-cache created

[root@k8s-master01 36-cicd]# kubectl create -f maven-cache.yaml
persistentvolumeclaim/maven-global-cache created

查看创建的pvc:

1
[root@k8s-master01 36-cicd]# kubectl get pvc

8.4准备Kaniko 的 Harbor 认证

在8.1的流水线模板中,Kaniko 容器需要挂载一个名为 docker-registry-config 的 ConfigMap 到 /kaniko/.docker,以便它有权限将构建好的镜像推送到 Harbor 仓库。

我的harbor信息如下:

1
2
3
地址:192.168.0.89
用户:admin
密码:Harbor12345

第一步:在本地登录 Harbor 仓库生成认证文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@k8s-master01 ~]# docker login 192.168.0.89
Username: admin
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores

Login Succeeded

#这会在/root/.docker/config.json. 中生成包含 auth 信息的认证文件。
[root@k8s-master01 ~]# cat .docker/config.json
{
"auths": {
"192.168.0.89": {
"auth": "YWRtaW46SGFyYm9yMTIzNDU="
}
}
}

第二步:将/root/.docker/config.json转化为k8s的ConfigMap

1
2
3
# 注意替换 -n 后面的命名空间,使其与 PVC 所在的命名空间一致,我这里都使用的默认命名空间
[root@k8s-master01 ~]# kubectl create configmap docker-registry-config --from-file=config.json=/root/.docker/config.json -n default
configmap/docker-registry-config created

9.自动化构建java应用

9.1创建java测试项目

该示例中用到的java项目可以从https://gitee.com/liujunweipy/spring-boot-project.git获取(也可以使用自己的java项目)

接下来将该项目导入到自己的 GitLab 中。首先在【群组】中找到之前创建的 Kubernetes 组,然后点击【新建项目】-【导入项目】-【仓库URL】-输入仓库地址点击【创建项目】,参考下图步骤:

仓库导入成功后如图所示:

9.2定义Jenkinsfile

Jenkinsfile 是用来保存 Pipeline 代码的文件,通常用代码仓库管理,或者直接放置于服务的 代码仓库中。Jenkinsfile 放置于代码仓库中,有以下好处:

  • 方便对流水线上的代码进行复查/迭代;
  • 对管道进行审计跟踪;
  • 流水线真正的源代码能够被项目的多个成员查看和编辑

接下来再 GitLab 的源代码中添加 Jenkinsfile。首先点击代码首页的“+”号,然后点击 New file:


在窗口中,添加通过模板更改后的 Pipeline,并且命名为 Jenkinsfile:


Jenkinisfile内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "pipeline-cache", readOnly: false)
yaml '''
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never
securityContext:
runAsUser: 0
fsGroup: 0
# nodeSelector:
# build: "true"
containers:
# jnlp容器,负责和Jenkins主节点通信,该容器中的配置不需要修改
- name: jnlp
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jnlp-agent-docker:latest
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
imagePullPolicy: IfNotPresent
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
# build容器,包含执行构建的命令,比如java的需要mvn构建,就可以用一个maven的镜像
- name: build #定义容器的名字,流水线的stage会用到改名字
# java程序使用maven镜像,包含mvn工具。如果是NodeJS项目可以使用node的镜像,根据实际情况修改
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/maven:3.5.3
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: maven-cache
mountPath: /root/.m2
readOnly: false
- name: localtime
mountPath: /etc/localtime
# kaniko 用来制作镜像的容器,可以是docker 或者其他可以用于构建镜像的工具
- name: kaniko
# 是示例中采用kaniko制作镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kaniko-executor:debug
imagePullPolicy: IfNotPresent
command:
- "sleep"
args:
- "99d"
tty: true
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
- name: docker-registry-config #挂载docker配置文件,用于kaniko连接镜像仓库
mountPath: /kaniko/.docker
readOnly: false
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 定义发版的镜像,因为最终是发版至 Kubernetes集群,所以需要有一个 kubectl 命令或者是其他的发版工具也可以,比如 helm,根据实际情况配置自己的发版镜像
- name: kubectl
# 发版的镜像根据实际需求进行变更,例如使用helm的镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kubectl:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
# 构建缓存的pvc
- name: maven-cache
persistentVolumeClaim:
claimName: maven-global-cache
readOnly: false
#docker认证信息的volume
- name: docker-registry-config
configMap:
name: docker-registry-config '''
}
}

//定义一些全局环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.0.89" //Harbor 地址
REGISTRY_DIR = "kubernetes" //Harbor 的项目目录
IMAGE_NAME = "spring-boot-project" //镜像的名称
NAMESPACE = "kubernetes" //该应用在 Kubernetes 中的命名空间
TAG = "" //镜像的 Tag,在此用 BUILD_TAG+COMMIT_ID 组成
GIT_URL = "git@192.168.0.222:kubernetes/spring-boot-project.git" //代码仓库地址
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue:'', description: 'Branch for build and deploy', name: 'BRANCH',quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',tagFilter: '*', type: 'PT_BRANCH')
}
// 接下来是拉代码的 stage,这个 stage 是一个并行的 stage,因为考虑了该流水线是手动触发还是触发
stages {
stage('拉取代码') {
steps {
script {
def branchToUse = env.gitlabBranch ?: params.BRANCH
// 加一个保护,防止两个来源都为空
if (!branchToUse) {
error "未指定构建分支,请检查触发方式或参数配置"
}
git(
url: "${GIT_URL}",
branch: branchToUse,
changelog: true,
poll: true,
credentialsId: 'gitlab-key'
)
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = "${env.BUILD_TAG}-${COMMIT_ID}"
echo "触发方式: ${env.gitlabBranch ? 'GitLab Webhook' : '手动触发'}"
echo "Branch: ${branchToUse}, Commit: ${COMMIT_ID}, Tag: ${TAG}"
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh """
mvn clean install -DskipTests
ls target/*
"""
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('部署到k8s') {
environment {
MY_KUBECONFIG = credentials('STUD_KUBECONFIG')
}
steps {
container(name: 'kubectl') {
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
}

9.3定义Dockerfile

在执行流水线过程时,需要将代码的编译产物做成镜像。而本次示例是 Java 项目,只需要 把 Jar 包放在有 Jre 环境的镜像中,然后启动该 Jar 包即可:


Dockerfile内容:

1
2
3
4
5
6
# 基础镜像可以按需修改,可以更改为公司自有镜像
FROM registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jre:8u211-data
# jar 包名称改成实际的名称,本示例为 spring-cloud-eureka-0.0.1-SNAPSHOT.jar
COPY target/spring-cloud-eureka-0.0.1-SNAPSHOT.jar ./
# 启动 Jar 包
CMD java -jar spring-cloud-eureka-0.0.1-SNAPSHOT.jar

9.4定义k8s资源

本案例在 GitLab 创建的 组 为 kubernetes,可以将其认为是一个项目,同一个项目可以 部署至 Kubernetes 集群中同一个 Namespace 中,本示例将服务部署至 kubernetes 命名空间。由于使用的是私 有仓库,因此需要先配置拉取私有仓库镜像的密钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 先创建kubernetes命名空间
[root@k8s-master01 ~]# kubectl create ns kubernetes
namespace/kubernetes created

# 创建拉取私有仓库镜像的secret,名字是harborkey
[root@k8s-master01 36-cicd]# kubectl create secret docker-registry harborkey --docker-server=192.168.0.89 --docker-username=admin --docker-password=Harbor12345 -n kubernetes
secret/harborkey created

# kubectl create secret 创建secret的指令
# docker-registry 表示创建 Secret 的类型,专门用于存储 Docker Registry 的认证信息。这是固定的类型名称,不需要修改。
# harborkey 这是 Secret 的名称,可以根据需要自定义
# --docker-server=192.168.0.89 #定义 Docker Registry 的服务器地址。【Docker Hub(默认)--docker-server=https://index.docker.io/v1/ 】 【私有 Registry:--docker-server=registry.mycompany.com 或者 --docker-server=192.168.1.100:5000 】
# --docker-username=admin 指定 Registry 的用户名是admin
# --docker-password=Harbor12345 指定admin用户的密码,这个用户要有仓库的权限才能拉取镜像,如果没有权限即使配置账号密码也不能拉取镜像

创建该应用的资源deployment+service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#我只创建了deployment和service,通过service的NodePort进行访问测试,就不用ingress了。
vim spring-boot-project.yaml
# 定义spring-boot-project资源的service
kind: Service
apiVersion: v1
metadata:
name: spring-boot-project #service的名称
namespace: kubernetes #service所在的命名空间
labels:
app: spring-boot-project #service的标签
spec:
type: NodePort
ports:
- port: 8761
targetPort: 8761
name: http
protocol: TCP
selector:
app: spring-boot-project #管理的pod的标签

---
## 定义spring-boot-project资源的deployment
kind: Deployment
apiVersion: apps/v1
metadata:
name: spring-boot-project #deployment的名称
namespace: kubernetes #deployment所在的命名空间
labels:
app: spring-boot-project #deployment的标签,和流水线中的set -l一致
spec:
replicas: 1
selector:
matchLabels:
app: spring-boot-project #具有该标签的pod才被deployment管理
template:
metadata:
labels:
app: spring-boot-project #这里定义的是pod标签
spec:
containers:
- name: spring-boot-project
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/nginx #这里用nginx作为原始镜像,通过Jenkins发版后会变成java应用的镜像
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: Asia/Shanghai
- name: LANG
value: zh_US.UTF-8
ports:
- containerPort: 8761
name: http
protocol: TCP
resources:
limits:
memory: "1280Mi"
cpu: "500m"
requests:
memory: "64Mi"
cpu: "250m"
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: harborkey #这里定义拉取镜像所用的secret,需要和上述创建的Secret名称一致
restartPolicy: Always

创建该资源:

1
2
3
4
5
6
7
8
[root@k8s-master01 36-cicd]# kubectl create -f spring-boot-project.yaml
service/spring-boot-project created
deployment.apps/spring-boot-project created

# pod已经创建,此时镜像是nginx,通过jenkins发版来更改为java镜像
[root@k8s-master01 36-cicd]# kubectl get deployments.apps -n kubernetes
NAME READY UP-TO-DATE AVAILABLE AGE
spring-boot-project 1/1 1 1 67s

9.5创建Jenkins任务-Job

单击首页的创建任务(Job)选项(或者是 New Item)并配置任务信息,如图所示:


输入的 Job 的名称(一般和 GitLab 的仓库名字一致,便于区分),类型为 Pipeline,最后点 击 OK 即可:

在新页面,点击 Pipeline,输入 Git 仓库地址、选择之前配置 GitLab Key、分支为 master, 点击 Save 即可:

如果遇到如下报错,关闭 Host Key 校验,步骤【Dashboard】-【Manage Jenkins】-【Security】中进行配置,参考下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. Branch Specifier (分支说明符)
它是根据什么写的?
它是严格根据你要拉取的 Git 代码仓库中的实际分支名称 来写的。Jenkins 需要确切地知道,当它执行构建时,应该检出(Checkout)哪一条代码线。
应该如何写?
*/master 或 */main:这是最常见的写法,代表拉取主分支。注意:早期 Git 项目主分支默认叫 master,而现在很多新项目(如 GitHub/GitLab 上的新仓库)默认叫 main。你需要根据你实际代码库的主分支名字来决定。
*/dev 或 */test:如果你当前配置的这个 Jenkins 任务是专门用来发布测试环境的,那你通常需要把它指向开发或测试分支。
完全留空 (blank for 'any'):如果你把这里的内容删掉,意味着 任意分支 发生代码更新,都会触发这条流水线。
通配符 */feature/*:如果你想让所有以 feature 开头的特性分支都能被构建,可以使用星号作为通配符。

2. Script Path (脚本路径)
这个名字定义的是什么?
它定义的是 Jenkinsfile 在你 Git 代码仓库中的“相对路径”和“文件名”。Jenkins 把指定分支的代码拉取下来后,要在茫茫代码中找到这本“说明书”才能开始干活。
应该怎么填?
默认情况是Jenkinsfile:这是标准的做法。它意味着你把文件直接放在了代码的根目录下,并且文件名字严格就叫 Jenkinsfile(注意首字母大写,没有 .txt 或 .groovy 等后缀)。
自定义文件名:有时为了区分不同环境,你可能会在项目里写多个文件,比如叫 Jenkinsfile.test 和 Jenkinsfile.prod。那么在配置生产环境的 Jenkins 任务时,这里就要相应改成 Jenkinsfile.prod。
放在子目录:如果你嫌根目录太乱,把 Jenkinsfile 放到了一个专门的 deploy/ 文件夹里,那这里就必须填写相对路径:deploy/Jenkinsfile。
一句话总结目前的配置: 按照你截图中的设置,Jenkins 的执行逻辑是——去你的代码库里拉取 master 分支 的代码,然后在代码的 根目录 下寻找名为 Jenkinsfile 的文件来执行流水线。如果你的代码库实际情况与此相符,直接保存即可。

配置完成后点击save保存,点击 Build Now(由于 Jenkins 参数由 Jenkinsfile 生成,所以第一次执行流水 线会失败),第一次构建结束后,可以看到 Build Now 变成了 Build with Parameters。点击 Build with Parameters 后,可以读取到 Git 仓库的分支,之后可以选择分支进行手动构建(后面的章节会介 绍自动触发):


选择分支,之后点击 Build,然后点击进度条即可看到构建日志:


构建日志上部分为创建 Pod 的日志,可以看到 Pod 为 Agent 指定的 Pod:


此时在 Kubernetes 中,会创建一个 Jenkins Slave 的 Pod:

1
2
3
[root@k8s-master01 ~]# kubectl get po
NAME READY STATUS RESTARTS AGE
spring-boot-project-41-73rvr-857cq-0zckt 4/4 Running 0 81s

待 Pod 启动后,Pipeline 任务就会执行,同时也可以点击 Blue Ocean 更加直观的流程:


如果是 Java 应用,同时可以看到 mvn 编译的过程:


编译结束后,可以看到制作镜像的日志:


最后为发版至 Kubernetes:


Finished:SUCCESS 说明该流水线正常结束。接下来可以查看 Deployment 的镜像,可以看到镜像中包含build id,说明是此次任务执行后的镜像:


如果配置了域名,可以通过域名访问(或者通过 Service 访问)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@k8s-master01 ~]# kubectl get all -n kubernetes
NAME READY STATUS RESTARTS AGE
pod/spring-boot-project-85886956d6-wx62b 1/1 Running 0 5m38s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/spring-boot-project NodePort 10.96.18.239 <none> 8761:32382/TCP 2d17h

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/spring-boot-project 1/1 1 1 2d17h

NAME DESIRED CURRENT READY AGE
replicaset.apps/spring-boot-project-5bbdfc7f69 0 0 0 2d17h
replicaset.apps/spring-boot-project-648dbd77c6 0 0 0 25h
replicaset.apps/spring-boot-project-74446fdcd9 0 0 0 18h
replicaset.apps/spring-boot-project-748fddf9b9 0 0 0 2d17h
replicaset.apps/spring-boot-project-85886956d6 1 1 1 5m38s
replicaset.apps/spring-boot-project-8697b8d595 0 0 0 18h
replicaset.apps/spring-boot-project-8d44d9d6 0 0 0 2d5h

我这里直接通过Service 访问,Service 的NodePort端口是32382:

10.自动化构建Vue/H5前端应用

本节介绍自动化构建 Vue/H5 应用,其构建方式和自动化构建 Java 基本相同,重点是更改 Deployment、Jenkinsfile 和 Dockerfile 即可。 前端应用测试项目地址:https://gitee.com/youlaiorg/vue3-element-admin.git,可以参考 Java 小节的方式, 导入前端项目到 GitLab 中,当然也可以使用公司自己的项目。

10.1创建vue测试项目

我这里用的是一个开源项目,将项目导入gitlab步骤参考9.1:

10.2定义Jenkinsfile

添加 Jenkinsfile 至项目根目录:


Vue 的Jenkinsfile 和 Java的Jenkinsfile 并无太大区别,需要更改的位置如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 编译镜像改为 NodeJS
image: "registry.cn-beijing.aliyuncs.com/k8s-liujunwei/node:lts"
imagePullPolicy: "IfNotPresent"
name: "build"
# 构建命令改为 npm
npm install --registry=https://registry.npmmirror.com/
npm run build:prod
# 代码地址和服务名称
IMAGE_NAME = "vue-project"
NAMESPACE = "kubernetes"
TAG = ""
GIT_URL = "git@192.168.0.222:kubernetes/vue-project.git"

修改后的Jenkinsfile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
//这里的工作目录单独定义一个pvc,不和java的共用一个,当然也可以共用,自己决定。
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "vue-pipeline-cache", readOnly: false)
yaml '''
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never
securityContext:
runAsUser: 0
fsGroup: 0
# nodeSelector:
# build: "true"
containers:
# jnlp容器,负责和Jenkins主节点通信,该容器中的配置不需要修改
- name: jnlp
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jnlp-agent-docker:latest
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
imagePullPolicy: IfNotPresent
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
# build容器,使用node的镜像来构建vue项目
- name: build #定义容器的名字,流水线的stage会用到改名字
#NodeJS项目可以使用node的镜像,如果是公司项目需要用你们自己对应的版本
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/node:lts
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
#- name: maven-cache # vue的项目这里用不到,注释掉
# mountPath: /root/.m2
# readOnly: false
- name: localtime
mountPath: /etc/localtime
# kaniko 用来制作镜像的容器,可以是docker 或者其他可以用于构建镜像的工具
- name: kaniko
# 是示例中采用kaniko制作镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kaniko-executor:debug
imagePullPolicy: IfNotPresent
command:
- "sleep"
args:
- "99d"
tty: true
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
- name: docker-registry-config #挂载docker配置文件,用于kaniko连接镜像仓库
mountPath: /kaniko/.docker
readOnly: false
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 定义发版的镜像,因为最终是发版至 Kubernetes集群,所以需要有一个 kubectl 命令或者是其他的发版工具也可以,比如 helm,根据实际情况配置自己的发版镜像
- name: kubectl
# 发版的镜像根据实际需求进行变更,例如使用helm的镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kubectl:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
# 构建缓存的pvc,vue的项目不在单独定义缓存目录了,直接用workspaceVolume定义的pvc即可
#- name: maven-cache
# persistentVolumeClaim:
# claimName: maven-global-cache
# readOnly: false
#docker认证信息的volume
- name: docker-registry-config
configMap:
name: docker-registry-config '''
}
}

//定义一些全局环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.0.89" //Harbor 地址
REGISTRY_DIR = "kubernetes" //Harbor 的项目目录
IMAGE_NAME = "vue-project" //镜像的名称,这里改成vue项目的名字
NAMESPACE = "kubernetes" //该应用在 Kubernetes 中的命名空间
TAG = "" //镜像的 Tag,在此用 BUILD_TAG+COMMIT_ID 组成
GIT_URL = "git@192.168.0.222:kubernetes/vue-project.git" //vue代码仓库地址
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue:'', description: 'Branch for build and deploy', name: 'BRANCH',quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',tagFilter: '*', type: 'PT_BRANCH')
}
// 接下来是拉代码的 stage,这个 stage 是一个并行的 stage,因为考虑了该流水线是手动触发还是触发
stages {
stage('拉取代码') {
steps {
script {
def branchToUse = env.gitlabBranch ?: params.BRANCH
// 加一个保护,防止两个来源都为空
if (!branchToUse) {
error "未指定构建分支,请检查触发方式或参数配置"
}
git(
url: "${GIT_URL}",
branch: branchToUse,
changelog: true,
poll: true,
credentialsId: 'gitlab-key'
)
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = "${env.BUILD_TAG}-${COMMIT_ID}"
echo "触发方式: ${env.gitlabBranch ? 'GitLab Webhook' : '手动触发'}"
echo "Branch: ${branchToUse}, Commit: ${COMMIT_ID}, Tag: ${TAG}"
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh '''
# build中的命令根据自己的项目进行配置
# ==================== 关键修复:强制使用 pnpm 9.x ====================
corepack enable pnpm # 启用 pnpm(只需一次)
corepack prepare pnpm@9 --activate # ← 这一行最重要!
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 # 避免 CI 环境卡住下载提示

# ==================== 下面保持不变 ====================
pnpm config set registry https://registry.npmmirror.com
pnpm install --frozen-lockfile #严格依据 pnpm-lock.yaml 安装依赖,禁止在安装过程中自动更新 lockfile。
pnpm run build
echo "✅ 构建完成!dist 文件夹已生成"
'''
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('部署到k8s') {
environment {
MY_KUBECONFIG = credentials('STUD_KUBECONFIG')
}
steps {
container(name: 'kubectl') {
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
}

10.3定义Dockerfile

前端应用构建后一般会在 dist 文件下产生 html 文件,只需要拷贝到 nginx 的根目录下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 基础镜像nginx
FROM registry.cn-beijing.aliyuncs.com/k8s-liujunwei/nginx:latest

# 删除默认的 nginx 配置
RUN rm /etc/nginx/conf.d/default.conf

# 拷贝自定义 nginx 配置
COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf

# 将构建后的产物拷贝到nginx的站点目录
COPY dist/ /usr/share/nginx/html/

# 暴露端口
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

新增Dockerfile,Dockerfile内容根据自己的情况写,我这里单独准备了nginx.conf的配置:


nginx.conf用来覆盖镜像中默认的配置文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;

# gzip 压缩
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary on;

# 处理 SPA 路由
location / {
try_files $uri $uri/ /index.html;
}

# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}

# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
}

10.4定义pvc

这里单独为vue项目准备了pvc,所以也要创建一个pvc资源,可以参考8.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@k8s-master01 36-cicd]# cat vue-cache.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
#pvc指定的名字要和流水线中workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "vue-pipeline-cache", readOnly: false) 配置的claimName引用的名字保持一致
name: vue-pipeline-cache
# 【重要提醒】:这里必须填写你的 Jenkins 动态 Agent Pod 所在的命名空间
# 如果你的 Jenkins 运行在 'jenkins' 命名空间,请改为 'jenkins'
namespace: default
spec:
volumeMode: Filesystem #卷模式要用文件系统,不指定默认是文件系统。 不能使用块存储
accessModes:
# CubeFS 支持多点读写,这对于并发构建非常重要,能防止卷被独占锁死
- ReadWriteMany
storageClassName: cfs-sc
resources:
requests:
# 分配 20G 给 Maven 本地仓库通常比较充裕,可根据实际团队规模调整
storage: 20Gi

创建pvc资源

1
2
[root@k8s-master01 36-cicd]# kubectl create -f vue-cache.yaml
persistentvolumeclaim/vue-pipeline-cache created

10.5定义k8s资源

对于 Kubernetes 的资源也是类似的,只需要更改资源名称,标签和端口号即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
[root@k8s-master01 36-cicd]# cat vue-project.yaml
#Deployment资源
kind: Deployment
apiVersion: apps/v1
metadata:
namespace: kubernetes
name: vue-project
labels:
app: vue-project
spec:
replicas: 1
selector:
matchLabels:
app: vue-project
template:
metadata:
labels:
app: vue-project
spec:
imagePullSecrets:
- name: harborkey #这里定义拉取镜像所用的secret,需要和9.4创建的Secret名称一致
containers:
- name: vue-project
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/nginx:latest
ports:
- containerPort: 80
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: TZ
value: "Asia/Shanghai"
- name: LANG
value: "en_US.UTF-8"
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
---
#定义Service资源
kind: Service
apiVersion: v1
metadata:
name: vue-project
namespace: kubernetes
spec:
type: NodePort
ports:
- port: 80
name: vue-project
targetPort: 80
selector:
app: vue-project

创建资源:

1
2
3
[root@k8s-master01 36-cicd]# kubectl create -f vue-project.yaml
deployment.apps/vue-project created
service/vue-project created

通过service的端口访问测试,是nginx的默认页面,说明部署成功:

1
2
3
4
[root@k8s-master01 36-cicd]# kubectl get svc -n kubernetes
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
spring-boot-project NodePort 10.96.18.239 <none> 8761:32382/TCP 3d15h
vue-project NodePort 10.96.79.157 <none> 80:32227/TCP 3m2s


接下来通过Jenkins来构建发版,部署为若依的前端项目

10.6创建Jenkins-Job

参考9.5


如果在构建时出现下图报错,记得设置该项目的部署密钥。


笔记演示的密钥是针对单个项目的,所以当新建项目是需要单独启用密钥,参考5.4。


执行构建:


发布成功后可以在浏览器访问该域名(或者使用 Service 访问):

11.自动化构建Golang项目

golang项目的代码来自开源的项目:https://github.com/golang/example.git

我已经把该项目同步到了gitlab中。

11.1golang项目

11.2定义Jenkinsfile

添加 Jenkinsfile 至项目根目录:


本次示例的 Jenkinsfile 和之前的也无太大区别,需要的更改的位置是构建容器的镜像、缓存 目录、Git 地址和项目名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
pipeline {
agent {
kubernetes {
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "golang-pipeline", readOnly: false) //这里用pvc单独定义了workspace
yaml '''
kind: Pod
apiVersion: v1
spec:
containers:
# build容器,包含执行构建的命令
- name: build #定义容器的名字,流水线的stage会用到改名字
# 使用golang镜像编译,版本根据自己的项目决定
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/golang:latest
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
# golang项目的缓存目录为/go/pkg/,执行 go build 时下载的依赖包会缓存在该目录
- name: golang-cache
mountPath: /go/pkg
readOnly: false
volumes:
# 构建缓存的pvc,
- name: golang-cache #这里单独定义了缓存用的存储,没有和workspace共用一个pvc
persistentVolumeClaim:
claimName: golang-global-cache
readOnly: false'''
}
}

//定义一些全局环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.0.89" //Harbor 地址
REGISTRY_DIR = "kubernetes" //Harbor 的项目目录
IMAGE_NAME = "go-project" //镜像的名称,这里改为golang项目的
NAMESPACE = "kubernetes" //该应用在 Kubernetes 中的命名空间
TAG = "" //镜像的 Tag,在此用 BUILD_TAG+COMMIT_ID 组成
GIT_URL = "git@192.168.0.222:kubernetes/go-project.git" //修改为golang代码仓库地址
}
// 接下来是拉代码的 stage,这个 stage 是一个并行的 stage,因为考虑了该流水线是手动触发还是触发
stages {
stage('Building') {
steps {
container(name: 'build') {
sh """ #这里构建的命令根据自己的项目修改
cd helloserver
export GO111MODULE=on
export CGO_ENABLED=0
export GOPROXY=https://goproxy.cn,direct
go build -v -o app
echo '✅ Go 构建完成!'
ls -lh app
"""
}
}
}
}

修改后完整的Jenkinsfile内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "golang-pipeline", readOnly: false)
yaml '''
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never
securityContext:
runAsUser: 0
fsGroup: 0
# nodeSelector:
# build: "true"
containers:
# jnlp容器,负责和Jenkins主节点通信,该容器中的配置不需要修改
- name: jnlp
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jnlp-agent-docker:latest
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
imagePullPolicy: IfNotPresent
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
# build容器,包含执行构建的命令,比如java的需要mvn构建,就可以用一个maven的镜像
- name: build #定义容器的名字,流水线的stage会用到改名字
# 使用golang镜像编译,版本根据自己的项目决定
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/golang:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: golang-cache
mountPath: /go/pkg
readOnly: false
- name: localtime
mountPath: /etc/localtime
# kaniko 用来制作镜像的容器,可以是docker 或者其他可以用于构建镜像的工具
- name: kaniko
# 是示例中采用kaniko制作镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kaniko-executor:debug
imagePullPolicy: IfNotPresent
command:
- "sleep"
args:
- "99d"
tty: true
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
- name: docker-registry-config #挂载docker配置文件,用于kaniko连接镜像仓库
mountPath: /kaniko/.docker
readOnly: false
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 定义发版的镜像,因为最终是发版至 Kubernetes集群,所以需要有一个 kubectl 命令或者是其他的发版工具也可以,比如 helm,根据实际情况配置自己的发版镜像
- name: kubectl
# 发版的镜像根据实际需求进行变更,例如使用helm的镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kubectl:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
# 构建缓存的pvc
- name: golang-cache
persistentVolumeClaim:
claimName: golang-global-cache
readOnly: false
#docker认证信息的volume
- name: docker-registry-config
configMap:
name: docker-registry-config '''
}
}

//定义一些全局环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.0.89" //Harbor 地址
REGISTRY_DIR = "kubernetes" //Harbor 的项目目录
IMAGE_NAME = "go-project" //镜像的名称
NAMESPACE = "kubernetes" //该应用在 Kubernetes 中的命名空间
TAG = "" //镜像的 Tag,在此用 BUILD_TAG+COMMIT_ID 组成
GIT_URL = "git@192.168.0.222:kubernetes/go-project.git" //代码仓库地址
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue:'', description: 'Branch for build and deploy', name: 'BRANCH',quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',tagFilter: '*', type: 'PT_BRANCH')
}
// 接下来是拉代码的 stage,这个 stage 是一个并行的 stage,因为考虑了该流水线是手动触发还是触发
stages {
stage('拉取代码') {
steps {
script {
def branchToUse = env.gitlabBranch ?: params.BRANCH
// 加一个保护,防止两个来源都为空
if (!branchToUse) {
error "未指定构建分支,请检查触发方式或参数配置"
}
git(
url: "${GIT_URL}",
branch: branchToUse,
changelog: true,
poll: true,
credentialsId: 'gitlab-key'
)
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = "${env.BUILD_TAG}-${COMMIT_ID}"
echo "触发方式: ${env.gitlabBranch ? 'GitLab Webhook' : '手动触发'}"
echo "Branch: ${branchToUse}, Commit: ${COMMIT_ID}, Tag: ${TAG}"
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh """
cd helloserver
export GO111MODULE=on
export CGO_ENABLED=0
export GOPROXY=https://goproxy.cn,direct
go build -v -o app
echo '✅ Go 构建完成!'
ls -lh app
"""
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('部署到k8s') {
environment {
MY_KUBECONFIG = credentials('STUD_KUBECONFIG')
}
steps {
container(name: 'kubectl') {
sh """
kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
"""
}
}
}
}
}

注意: kubernetes { workspaceVolume persistentVolumeClaimWorkspaceVolume(…) } 中声明的 workspaceVolumepod 级别的特殊卷

  • 插件会自动把这个卷(名字固定为 workspace-volume,挂载路径 /home/jenkins/agent)注入到 Pod 里的每一个容器,包括:

    • jnlp(默认)
    • build
    • kaniko
    • kubectl
  • 不需要在 yaml 里为每个容器手动写 volumeMounts 这一段。

    1
    2
    3
    4
    5
    - name: build  #(build容器)
    ...
    volumeMounts:
    - name: workspace-volume # ← 这行可以删掉
    mountPath: /home/jenkins/agent # ← 这行也可以删掉

这是插件内置的 DefaultWorkspaceVolume 装饰器自动完成的,不需要手动配置

11.3定义Dockerfile

和之前不一样的地方是,Golang 编译后生成的是一个二进制文件,可以直接执行,所以底 层镜像设置为 alpine 或者其它的小镜像即可:

1
2
3
4
5
6
7
8
9
10
#由于在 Jenkins 的 build 容器里已经完成了 Go 编译,产出的 app 是一个静态二进制文件(因为设置了 CGO_ENABLED=0),所以 Dockerfile 里其实不需要 Go 运行时环境,可以换成更小的基础镜像:
# FROM registry.cn-beijing.aliyuncs.com/k8s-liujunwei/golang:1.26-alpine
# 或者极致精简用:
FROM scratch
WORKDIR /app
# 拷贝 Jenkins 已经编译好的二进制
COPY helloserver/app .
# 如果你的程序监听 8080
EXPOSE 8080
CMD ["./app", "-addr", "0.0.0.0:8080"]

11.4定义pvc

同样单独为go项目准备了pvc,所以也要创建提前创建好pvc资源,可以参考8.3:

准备两个pvc资源,一个是Jenkins的任务的工作目录(workspace),一个是go项目构建依赖的缓存目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# golang项目流水线的工作目录
[root@k8s-master01 36-cicd]# cat go-project.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: golang-pipeline
# 【重要提醒】:这里必须填写你的 Jenkins 动态 Agent Pod 所在的命名空间
# 如果你的 Jenkins 运行在 'jenkins' 命名空间,请改为 'jenkins'
namespace: default
spec:
volumeMode: Filesystem #卷模式要用文件系统,不指定默认是文件系统。 不能使用块存储
accessModes:
# CubeFS 支持多点读写,这对于并发构建非常重要,能防止卷被独占锁死
- ReadWriteMany
storageClassName: cfs-sc
resources:
requests:
# 分配 20G 给 Maven 本地仓库通常比较充裕,可根据实际团队规模调整
storage: 20Gi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# golang项目构建缓存目录
[root@k8s-master01 36-cicd]# cat golang-cache.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: golang-global-cache
# 【重要提醒】:这里必须填写你的 Jenkins 动态 Agent Pod 所在的命名空间
# 如果你的 Jenkins 运行在 'jenkins' 命名空间,请改为 'jenkins'
namespace: default
spec:
volumeMode: Filesystem #卷模式要用文件系统,不指定默认是文件系统。 不能使用块存储
accessModes:
# CubeFS 支持多点读写,这对于并发构建非常重要,能防止卷被独占锁死
- ReadWriteMany
storageClassName: cfs-sc
resources:
requests:
# 分配 20G 给 Maven 本地仓库通常比较充裕,可根据实际团队规模调整
storage: 20Gi

创建pvc资源:

1
2
3
[root@k8s-master01 36-cicd]# kubectl create -f golang-cache.yaml -f golang-pipeline.yaml
persistentvolumeclaim/golang-global-cache created
persistentvolumeclaim/golang-pipeline created

11.5定义k8s资源

对于 Kubernetes 的资源也是类似的,只需要更改资源名称,标签和端口号即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 定义go-project资源的service
kind: Service
apiVersion: v1
metadata:
name: go-project #service的名称
namespace: kubernetes #service所在的命名空间
labels:
app: go-project #service的标签
spec:
type: NodePort
ports:
- port: 8080
targetPort: 8080
name: web
protocol: TCP
selector:
app: go-project #管理的pod的标签

---
## 定义go-project资源的deployment
kind: Deployment
apiVersion: apps/v1
metadata:
name: go-project #deployment的名称
namespace: kubernetes #deployment所在的命名空间
labels:
app: go-project #deployment的标签,和流水线中的set -l一致
spec:
replicas: 1
selector:
matchLabels:
app: go-project #具有该标签的pod才被deployment管理
template:
metadata:
labels:
app: go-project #这里定义的是pod标签
spec:
containers:
- name: go-project
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/nginx #这里用nginx作为原始镜像,通过Jenkins发版后会变成go应用的镜像
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: Asia/Shanghai
- name: LANG
value: zh_US.UTF-8
ports:
- containerPort: 8080 #go服务的端口号是8080
name: http
protocol: TCP
resources:
limits:
memory: "1280Mi"
cpu: "500m"
requests:
memory: "64Mi"
cpu: "250m"
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: harborkey #这里定义拉取镜像所用的secret,需要和上述创建的Secret名称一致
restartPolicy: Always

创建资源:

1
[root@k8s-master01 36-cicd]# kubectl create -f go-project.yaml
1
2
3
4
5
6
[root@k8s-master01 36-cicd]# kubectl get -f go-project.yaml
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/go-project NodePort 10.96.39.151 <none> 8080:30913/TCP 42h

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/go-project 1/1 1 1 42h

11.6创建Jenkins-Job


执行任务:


任务完成后检查pod的镜像是否更新,并访问web页面:

1
2
3
4
5
6
[root@k8s-master01 36-cicd]# kubectl get po -n kubernetes go-project-87d44d78c-7p7mr -oyaml|grep image
image: 192.168.0.89/kubernetes/go-project:jenkins-go-project-1-8cdd1a2
imagePullPolicy: IfNotPresent
imagePullSecrets:
image: 192.168.0.89/kubernetes/go-project:jenkins-go-project-1-8cdd1a2
imageID: 192.168.0.89/kubernetes/go-project@sha256:a5218fc2dd54c6e79f6768eefd578e85a6c2033b87100979abba041895ebf605
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@k8s-master01 36-cicd]# kubectl get all -n kubernetes
NAME READY STATUS RESTARTS AGE
pod/go-project-87d44d78c-7p7mr 1/1 Running 0 2m53s
pod/spring-boot-project-756bd77956-pfnmj 1/1 Running 0 11d
pod/vue-project-7f4b645b5d-pd726 1/1 Running 0 11d

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/go-project NodePort 10.96.39.151 <none> 8080:30913/TCP 42h
service/spring-boot-project NodePort 10.96.18.239 <none> 8761:32382/TCP 15d
service/vue-project NodePort 10.96.177.229 <none> 80:31512/TCP 11d

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/go-project 1/1 1 1 42h
deployment.apps/spring-boot-project 1/1 1 1 15d
deployment.apps/vue-project 1/1 1 1 11d

通过service/go-project的30913端口访问:


看到上图表示发布成功!

12.自动触发构建部署

之前的构建都是采用手动选择分支进行构建的,实际使用时,项目可能有很多,如果都是手 动触发可能比较消耗人力。所以推荐可以按需配置自动触发,即提交代码后自动触发 Jenkins 进 行构建任务。

本次使用上述Go项目进行演示。

12.1配置GitLab允许触发外部接口

要想实现自动触发构建,首先在gitlab上开启该项目允许触发自动构建的按钮按照下图进行勾选:

管理中心-设置-网络-出站请求-勾选”允许来自 webhooks 和集成对本地网络的请求”-保存更改


12.2配置Jenkins的Job

接着进入Jenkins项目继续配置Job—Configure-Triggers-

点击要配置的项目,这里以go的项目为例:


触发器讲解:Triggers

  • Build after other projects are built:当指定的上游 Jenkins 项目构建完成后,自动触发当前项目构建

  • Build periodically:按照固定时间表定时触发构建,不管代码是否有变化,类似 Linux定时任务(crontab) 。

    1
    2
    3
    4
    5
    6
    7
    8
    # 语法格式: 分  时  日  月  周
    * * * * *
    常用示例:
    H 2 * * * → 每天凌晨2点构建一次
    H/15 * * * * → 每15分钟构建一次
    H 8 * * 1-5 → 工作日每天早上8点
    0 8,20 * * * → 每天8点和20点各一次
    H H * * 0 → 每周日随机某时刻构建
  • Build when a change is pushed to GitLab. GitLab webhook URL: http://192.168.0.87:8080/project/go-project:这是 GitLab Webhook 触发的核心开关。勾选后 Jenkins 会生成一个 Webhook URL(截图中显示的 http://192.168.0.87:8080/project/go-project),需要将这个 URL 填入 GitLab 仓库的 Webhook 设置中。这是 GitLab + Jenkins 集成的最主流方案

  • GitHub hook trigger for GITScm polling:专门为 GitHub 设计的 Webhook 触发器,与第3条类似但针对 GitHub 平台。GitHub 在检测到 Push 事件时通知 Jenkins

  • Poll SCM:Jenkins 主动定期去检查 Git 仓库是否有新提交,有变化才触发构建。是一种拉取(Pull)模式,与 Webhook 的推送(Push)模式相反

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 配置语法(同 cron)
    # 每5分钟检查一次仓库是否有新提交
    H/5 * * * *

    # 工作时间(9-18点)每分钟检查
    * 9-18 * * 1-5

    # 每小时检查一次
    H * * * *
  • Trigger builds remotely:通过携带 Token 的 HTTP 请求来触发构建,任何能发 HTTP 请求的程序、脚本、系统都可以触发 Jenkins 构建。

选择第三种最主流的方式,也是生产中常用的方式:Build when a change is pushed to GitLab. GitLab webhook URL


GitLab 触发事件细分:

选项 含义 适用场景
Push Events 有代码 push 时触发 最常用,开发提交代码即触发CI
Push Events in case of branch delete 分支被删除时也触发 一般不需要,避免误触发
Opened Merge Request Events 创建合并请求(MR)时触发 代码审查前自动跑测试
Build only if new commits pushed to Merge Request 仅 MR 有新提交时触发 避免重复构建同一 MR
Accepted Merge Request Events MR 被合并时触发 合并到主干后自动部署
Closed Merge Request Events MR 被关闭(未合并)时触发 较少使用


Rebuild open Merge Requests:表示当代码发生变化时,决定是否要对所有”仍处于开放状态的 MR”重新触发一次构建

  • Never(永不重建):只对直接触发事件的那个 MR 构建,不影响其他开放中的 MR,常用该选项。

  • On push to source branch(推送到源分支时重建):当任意一个 MR 的源分支(即开发分支)有新代码推送时,重新触发所有开放 MR 的构建。

  • On push to source or target branch(推送到源或目标分支时重建):在第2个选项的基础上,额外增加:当目标分支(通常是 main/develop)有新代码合入时,也重新触发所有开放 MR 的构建。

    案例:假设你有这样一个仓库:

    1
    2
    main 分支(主干)
    feature-login 分支(开发分支)

    并且存在一个 open 状态的 MR

    feature-loginmain(即:source branch = feature-login,target branch = main)

    • On push to source branch

      含义:当有人 push 到这个 MR 的源分支时,重新触发该 MR 的构建

      1
      2
      3
      # 开发者在 feature-login 分支上修了个 bug,push 上去
      git commit -m "fix: 修复登录验证逻辑"
      git push origin feature-login ✅ 触发
    • On push to source or target branch

      含义:无论 push 到源分支还是目标分支,都重新触发该 MR 的构建

      1
      2
      3
      4
      5
      6
      # 案例一:push 到 source branch(feature-login分支)
      git push origin feature-login → ✅ 触发自动构建

      #案例二:push 到 target branch(main分支)
      # 另一个同事合并了其他内容到 main,main 分支有了新代码
      git push origin main → ✅ 也触发自动构建

      注意:这里的On push to source branchOn push to source or target branch功能要起作用前提是要开启相关MR触发构建的功能,在该案例中只选择了Push Events作为触发条件,所以如果选择了这俩参数也是不起作用的。

  • Approved Merge Requests (EE-only):GitLab 企业版(EE)专属功能。当 MR 获得足够数量的审批人批准后,自动触发 Jenkins 构建。保持默认即可。
  • Comments:允许通过在 GitLab MR页面发评论的方式触发 Jenkins 构建,开发者在 GitLab MR 评论区输入: “Jenkins please retry a build” 来触发构建,”Jenkins please retry a build” 要想起作用要勾选Comments选项,同时还要勾选MR触发条件,该案例中只勾选了Push Events作为触发条件,所以这里的配置实际不会触发自动构建。

Advanced高级选项中参数解释:

  • Enable [ci-skip]:

    作用: 允许通过在 commit message 中添加 [ci-skip] 来跳过 CI 构建。

    实际案例:

    开发者如果只是修改了 README.md 文档,或者想跳过此次构建,不需要触发构建,提交时写:

    1
    git commit -m "update README [ci-skip]"

    Jenkins 收到 Push 后识别到 [ci-skip]直接跳过构建,节省资源。

  • Ignore WIP Merge Requests

    作用: 忽略标记为 WIP(Work In Progress)的 MR,不触发构建。

    实际案例:

    开发者创建了一个还未完成的 MR,标题为:

    1
    WIP: feature/payment-module

    由于勾选了此项,Jenkins 不会对该 MR 触发构建,等开发者去掉 WIP 标记后才会正式触发。该功能同样需要勾选MR作为触发条件才会有作用,只勾选了Push Events该条件即使勾选了也不会起作用

  • Labels that launch a build if they are added(空)

    作用: 当 MR 被添加指定 Label 时触发构建,多个 Label 用逗号分隔。

    实际案例:

    填写 ready-for-ci, trigger-build 当 Reviewer 在 MR 上打上 ready-for-ci 标签时,Jenkins 自动触发构建,适合需要人工审核后再构建的团队流程。

  • Set build description to build cause

    作用: 将构建原因(如 Merge Request 或 Git Push)自动设置为 Jenkins 构建的描述信息。

    实际案例:

    触发构建后,在 Jenkins 构建历史中可以看到:

    1
    Build #42 Started by GitLab push by Administrator

    方便团队成员快速了解每次构建是由什么操作引起的,便于追溯。

    Set build description to build cause 实际效果

    勾选该选项后,Jenkins 会根据实际触发来源自动生成描述,常见的真实格式为:

    触发方式 实际显示
    Push 触发 Started by GitLab push by Administrator
    MR 触发 Started by GitLab Merge Request by Administrator
    评论触发 Started by GitLab Note by Administrator
  • Allowed branches:表示 哪些 Git 分支允许触发 Jenkins

    • Allow all branches to trigger this job:所有分支都允许触发 Jenkins

    • Filter branches by name:精确匹配分支名来触发构建

    • Filter branches by regex(常用):基于正则匹配分支以实现触发构建

      • Source Branch Regex:意思是源分支匹配,在 Push Event 下,它其实就是当前 push 的分支,例如执行git push origin master,那么Source Branch = master,也就是所只有当push代码到master分支时会触发自动构建。
      • Target Branch Regex:这个参数 主要是给 Merge Request 用的。例如当feature/login分支 MR 到dev分支时,那么Source Branch=feature/loginTarget Branch= dev。该笔记中没有启动MR,所以这里Target Branch Regex不生效。

      如果启用了MR的作为触发条件,Source Branch RegexTarget Branch Regex真正的含义如下:

      • Source Branch Regex :表示 MR 从哪个源分支发起,例如 feature/login -> dev,那么Source Branch = feature/login
      • Target Branch Regex:表示 MR 要合并到哪个目标分支,例如 feature/login -> dev,那么Target Branch = dev

      实际上企业里一般不会 Push 和 MR 用在同一个 Job流水线中,Push和MR本质上是两种完全不同的工作流,两者语义不同,企业通常会把 Push 和 MR 拆成两个 Jenkins Job。

12.3配置Gitlab

在gitlab的项目中配置webhook,主要配置URL,Secret令牌和 触发来源如下图:


URL来源于:Jenkins项目中触发器中的Build when a change is pushed to GitLab. GitLab webhook URL: http://192.168.0.87:8080/project/go-project


Secret令牌来源于:触发器(Triggers)中生成的Secret token


触发来源选择所有分支,其他保持默认保存即可


配置成功显示如下:


测试触发功能:测试前Jenkins上的任务序号是7,容器中的镜像对应的也是7,如下图:


接下来点击webhooks中的**[测试]—[推送事件]**,观察Jenkins上的任务以及容器的镜像是否有变化:


Jenkins上的任务已经自动触发,序号为8,等待任务结束检查容器镜像是否发布为新的,镜像中包含8


镜像已经重新发布:


也可以在Harbor镜像仓库中看到此镜像:


自动触发构建结束!

13.一次构建多次部署

在企业内部署服务时,往往每个项目都有多个环境,比如 dev、sit、prod 等。但是并非每个 环境部署时,都需要进行重新编译、构建镜像等,此时可以把 dev 的镜像直接部署至 sit 和 prod。


依然使用go项目进行演示:在Jenkins上创建一个新的job,名字为go-project-prod,类型为pipeline:


点击勾选页面的This project is parameterized-Image Tag Parameter(参数化构建):


选择参数类型为 Image Tag Parameter(需要安装 Image Tag 插件),之后定义 Name 为变量的名称,Iamge Name 为 Harbor 的目录和镜像名称,参考下图配置:

  • Name中的IMAGE_TAG是定义一个变量,用来获取用户在Jenkins前台选择的值,随意命名,但要和 Pipeline 脚本里引用的保持一致。

  • Image Name 中的 kubernetes/go-project是告诉插件要从哪个镜像仓库获取镜像 tag 列表,在 Harbor 中对应 项目名/仓库名。必须和 Harbor 上存在的镜像路径一致,否则插件无法拉取 tag。对应的Harbor信息如下图

  • Tag Filter Pattern中的 .*用正则匹配要显示的镜像 tag,.* 表示匹配全部 tag,^release-.* 表示只匹配 release- 开头的 tag。

  • Default Tag中的latest表示默认选择的 tag,Build With Parameters 页面加载时默认填充。

  • Description是在前台 Build With Parameters 页面显示的提示文字,告诉用户该参数是做什么的。

  • Advanced 高级中的部分:

    • Registry URL:指定 Harbor 仓库地址,如果是HTTP或者 HTTPS 且自签名证书,不要勾选 “Verify SSL”。
    • Registry Credential ID:Jenkins 插件使用的登录凭据,用于私有仓库认证,建议使用对应 Harbor 的 Robot Account 。
    • Tag Ordering:下拉列表里 tag 的排序方式

该流水线的pipeline脚本内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
yaml '''
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never
securityContext:
runAsUser: 0
fsGroup: 0
containers:
# 只需要配置jnlp和kubectl容器即可。
- name: jnlp
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jnlp-agent-docker:latest
args: ["$(JENKINS_SECRET)", "$(JENKINS_NAME)"]
imagePullPolicy: IfNotPresent
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
- name: kubectl
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kubectl:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
'''
}
}

environment {
HARBOR_ADDRESS = "192.168.0.89" //定义Harbor仓库的地址
IMAGE_NAME = "go-project" //镜像的名字
K8S_NAMESPACE = "kubernetes" //定义的是k8s中的命名空间
}

stages {
stage('部署到k8s') {
environment {
MY_KUBECONFIG = credentials('STUD_KUBECONFIG')
}
steps {
container(name: 'kubectl') {
sh '''
#这里FULL_IMAGE拼接后的值是192.168.0.89/kubernetes/go-project:jenkins-go-project-9-05efde4,${IMAGE_TAG}是在前台选择的值
FULL_IMAGE="${HARBOR_ADDRESS}/${IMAGE_TAG}"
echo "部署到环境中的镜像是:${FULL_IMAGE}"
kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${FULL_IMAGE} -n ${K8S_NAMESPACE}
'''
}
}
}
}
}

---------------------------------------------------------------------------------
# pipeline语法中部分参数解释:
kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${FULL_IMAGE} -n ${K8S_NAMESPACE}
# set image 用于更新 Pod 模板中一个或多个容器的镜像指令
# deployment/${IMAGE_NAME} 中的deployment表示资源的类型。表示要操作的是一个 Deployment 控制器,/是分隔符,${IMAGE_NAME}是deployment的名字,取自pipeline中的变量。
# ${IMAGE_NAME}=${FULL_IMAGE}${IMAGE_NAME}表示这个deployment中容器的名字,${FULL_IMAGE}表示镜像的地址。
# -n ${K8S_NAMESPACE} 指定命名空间,值都是来自于pipeline中定义的变量

假设现在需要将版本发布为jenkins-go-project-8-05efde4,点击Build with parameters,在选在需要的版本,点击Build即可。


Jenkins控制台日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Masking supported pattern matches of $MY_KUBECONFIG
[Pipeline] {
[Pipeline] container
[Pipeline] {
[Pipeline] sh
+ FULL_IMAGE=192.168.0.89/kubernetes/go-project:jenkins-go-project-8-05efde4
+ echo 部署到环境中的镜像是:192.168.0.89/kubernetes/go-project:jenkins-go-project-8-05efde4
部署到环境中的镜像是:192.168.0.89/kubernetes/go-project:jenkins-go-project-8-05efde4
+ kubectl --kubeconfig **** set image deployment/go-project go-project=192.168.0.89/kubernetes/go-project:jenkins-go-project-8-05efde4 -n kubernetes
deployment.apps/go-project image updated
[Pipeline] }
[Pipeline] // container
[Pipeline] }
[Pipeline] // withCredentials
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // withEnv
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // podTemplate
[Pipeline] End of Pipeline
Finished: SUCCESS

检查容器的镜像是否已经变更为jenkins-go-project-8-05efde4:

1
2
3
4
5
6
[root@k8s-master01 36-cicd]# kubectl get po -n kubernetes go-project-59b66bcc6b-2rz6r -oyaml|grep image
image: 192.168.0.89/kubernetes/go-project:jenkins-go-project-8-05efde4
imagePullPolicy: IfNotPresent
imagePullSecrets:
image: 192.168.0.89/kubernetes/go-project:jenkins-go-project-8-05efde4
imageID: 192.168.0.89/kubernetes/go-project@sha256:39716520cc17788c1b678b9af709e8ff89c827ce7fb3c97ed17afcd91531d2d2

到此一次构建多次部署的任务已经实现,下面是我实际配置中遇到的报错以及解决方法:

按照下图配置后点击Build with parameters会出现一个HTTP status Unauthorized的报错,并且获取不到Harbor仓库的镜像。

在上图的配置中,Registry Credential ID的值选择的是步骤5.3中配置的账号密码,image tag parameter 插件可能存在不兼容的bug,导致获取不到harbor的镜像,可能会下图的报错

原因是私有仓库的 Token 认证流程上。插件拿到 Token 后请求私有仓库时 scope 不对。

解决办法是 Harbor 的 Robot Account 使用的是简单 Basic Auth,不走 Bearer Token 流程,可以绕开插件的 Token 解析 bug。

使用 Harbor Robot Account(官方推荐)

  • 创建一个 Robot Account 专门给 Jenkins 使用,权限只给 Pull

  • Robot Account 生成的用户名和 token 可以作为 Jenkins Credential 使用。

  • 好处:

    • 私有仓库仍然安全。
    • 避免 admin token 可能被暴露。
    • 插件大部分版本对 Robot Account 更兼容。
  • 步骤:

    1. Harbor → 项目 → Robot Accounts → New Robot Account

    2. 权限选择 Pull

    3. 在 Jenkins → Credentials 添加用户名/密码类型:

      1
      2
      Username: robot$xxx
      Password: Harbor 自动生成的 token

    4. Image Tag Parameter 配置 Registry Credential ID 为这个 robot 账号

    5. 再次尝试 Build with Parameters即可解决上述报错。

14.集成Helm发版

虽然上述中的笔记已经借助流水线实现了服务的自动发版,但是针对 Kubernetes 的资源依旧是提前手动创建好,如果微服务很多这样一个一个手动创建就不太友好,下面将借助 Helm Charts 实现微服务资源的自动创建和发版。

整体链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
开发提交代码到 GitLab
|
v
Jenkins 拉代码并构建镜像
|
v
镜像推送到 Harbor
|
v
Jenkins 进入项目里的 chart 目录
|
v
helm upgrade --install
|
v
Helm 根据 values.yaml + --set 参数生成 K8s YAML
|
v
部署 Deployment / Service / Ingress 到 K8s

如果没有 Helm,每个服务都要维护一堆类似的 YAML:

1
2
3
4
5
deployment.yaml
service.yaml
ingress.yaml
configmap.yaml
secret.yaml

而且每个服务大部分内容都差不多,只是镜像名、端口、域名、副本数、资源限制不同。

Helm 的作用就是把这些 YAML 做成模板:

1
2
3
4
5
6
镜像仓库地址     可以变
镜像 tag 可以变
服务名 可以变
副本数 可以变
Ingress 域名 可以变
资源限制 可以变

所以 Jenkins 发版时只需要传入当前服务的变量,例如:

1
2
3
--set image.repository=xxx/go-project
--set image.tag=20260609-001
--set replicaCount=1

Helm 就会自动生成最终的 Kubernetes YAML,并提交给 K8s。

14.1创建helm模板

第一步:创建通用 Helm Chart 模板

1
2
3
4
5
6
[root@k8s-master01 chart-cicd]# helm create chart-template
Creating chart-template

[root@k8s-master01 chart-cicd]# ll
total 0
drwxr-xr-x 4 root root 93 Jun 14 14:42 chart-template

第二步:修改配置

接下来对模板稍加修改,使其变成一个通用的模板。

  • 首先更改默认拉取镜像的 Secret:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 编辑模板中的values.yaml文件
    [root@k8s-master01 chart-cicd]# cd chart-template/

    [root@k8s-master01 chart-template]# ll
    total 12
    drwxr-xr-x 2 root root 6 Jun 14 14:42 charts
    -rw-r--r-- 1 root root 1150 Jun 14 14:42 Chart.yaml
    drwxr-xr-x 3 root root 184 Jun 14 14:42 templates
    -rw-r--r-- 1 root root 5280 Jun 14 14:42 values.yaml

    # 在17行imagePullSecrets字段下添加拉取镜像的secret(18行),注意格式
    [root@k8s-master01 chart-template]# vim values.yaml
    16 # This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure
    -pod-container/pull-image-private-registry/
    17 imagePullSecrets:
    18 - name: harborkey
    19 # This is to override the chart name.
    20 nameOverride: ""
    21 fullnameOverride: ""
  • 关闭 ServiceAccount

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 将26行的create: true 改为 create: false
    [root@k8s-master01 chart-template]# vim values.yaml
    23 # This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
    24 serviceAccount:
    25 # Specifies whether a service account should be created.
    26 create: false #将这里原本的true改为false
    27 # Automatically mount a ServiceAccount's API credentials?
    28 automount: true
    29 # Annotations to add to the service account.
    30 annotations: {}
    31 # The name of the service account to use.
    32 # If not set and create is true, a name is generated using the fullname template.
    33 name: ""

    Helm 默认模板会尝试给每个服务创建一个 ServiceAccount。

    在该场景中,微服务不需要单独的 ServiceAccount,使用 namespace 里的默认账号就够了。

  • 修改服务的资源 requests / limits

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 新增了116~121行,资源配额根据自己情况修改
    [root@k8s-master01 chart-template]# vim values.yaml
    115 resources:
    116 limits:
    117 cpu: 1000m
    118 memory: 512Mi
    119 requests:
    120 cpu: 200m
    121 memory: 256Mi
    122 # We usually recommend not to specify default resources and to leave this as a conscious
    123 # choice for the user. This also increases chances charts run on environments with little
    124 # resources, such as Minikube. If you do want to specify resources, uncomment the following
    125 # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    126 # limits:
    127 # cpu: 100m
    128 # memory: 128Mi
    129 # requests:
    130 # cpu: 100m
    131 # memory: 128Mi
  • 修改服务的端口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 因为go-project服务的端口是8080,根据自己服务的端口进行修改
    [root@k8s-master01 chart-template]# vim values.yaml
    # This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
    service:
    # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
    type: ClusterIP
    # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
    port: 8080 #这里的端口会在templates/deployment.yaml中定义容器的部分引用,健康检查会探测该端口,如果和服务的端口不匹配会导致容器无法启动

  • 把健康检查改成 tcpSocket

    为什么用 tcpSocket

    因为模板是通用的,它未必知道每个服务的健康检查 HTTP 路径。每个服务的健康检查路径可能都不一样,有的服务可能是:

    1
    2
    3
    4
    /health
    /actuator/health
    /api/health
    /ping

    如果写死 HTTP path,容易不通用。

    tcpSocket 的逻辑是:只要服务端口能连上,就认为服务基本可用。

    这比 HTTP 健康检查弱一点,但更通用。后续如果某个服务有标准健康检查接口,可以单独在它自己的 values.yaml 里改成 HTTP 类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 将健康检查修改为tcpSocket
    [root@k8s-master01 chart-template]# vim values.yaml
    # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure
    -liveness-readiness-startup-probes/
    livenessProbe:
    tcpSocket:
    port: http #这里的port不一定是数字,也可以是容器端口名称(Named Port),这里的http字段要和pod中pod.spec.containers.ports.name字段定义的保持一致,
    # httpGet:
    # path: /
    # port: http
    readinessProbe:
    tcpSocket:
    port: http
    # httpGet:
    # path: /
    # port: http

    解释tcpSocket中的port字段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    livenessProbe:
    tcpSocket:
    port: http
    #这里的port不一定是数字,也可以是容器端口名称(Named Port),这里的http字段要和pod中pod.spec.containers.ports.name字段定义的保持一致,在helm chart的模板中该字段的值在templates/deployment.yaml中定义的,在44行中固定写死的
    35 containers:
    36 - name: {{ .Chart.Name }}
    37 {{- with .Values.securityContext }}
    38 securityContext:
    39 {{- toYaml . | nindent 12 }}
    40 {{- end }}
    41 image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
    42 imagePullPolicy: {{ .Values.image.pullPolicy }}
    43 ports:
    44 - name: http
    45 containerPort: {{ .Values.service.port }}
    46 protocol: TCP

第三步:测试模板是否可用

1
2
3
4
5
6
7
8
9
10
11
12
# 测试修改后的模板是否可用
helm template test . \
--set fullnameOverride=test \
--set nameOverride=test \
--set ingress.enabled=true \
--set ingress.className=nginx \
--set ingress.hosts[0].host=test.com \
--set ingress.hosts[0].paths[0].path=/api/ \
--set 'ingress.annotations.kubernetes\.io/ingress\.class'=nginx \
--set 'ingress.annotations.kubernetes\.io/tls-acme'=true >test.yaml

#通过追重定向想内容输出到test.yaml文件中,方便检查生成的配置

14.2流水线中集成helm

接下来为每个项目复制一份chart,意思是把通用模板复制成某个项目自己的 Chart。这里还是使用 go-project 为例进行演示,生成一个 go-project 的 Chart:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 先将项目代码拉取到本地,目的是将chart模板添加到代码中,在推送到gitlab仓库中
[root@k8s-master01 chart-cicd]# git clone http://192.168.0.222/kubernetes/go-project.git
Cloning into 'go-project'...
Username for 'http://192.168.0.222': root
Password for 'http://root@192.168.0.222':
remote: Enumerating objects: 138, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 138 (delta 6), reused 0 (delta 0), pack-reused 122
Receiving objects: 100% (138/138), 114.39 KiB | 6.35 MiB/s, done.
Resolving deltas: 100% (31/31), done.

[root@k8s-master01 chart-cicd]# ll
total 4
drwxr-xr-x 4 root root 93 Jun 15 23:23 chart-template
drwxr-xr-x 12 root root 4096 Jun 15 23:25 go-project

# 将chart-template目录中的所有文件复制一份到go-project目录下的chart目录中,末尾的 / 是灵魂:源路径 chart-template/ 末尾的斜杠表示“复制该目录下的内容”。如果不加斜杠,它会把整个 chart-template 文件夹作为子目录塞进去。
[root@k8s-master01 chart-cicd]# rsync -av chart-template/ go-project/chart/
sending incremental file list
created directory go-project/chart
./
.helmignore
Chart.yaml
values.yaml
charts/
templates/
templates/NOTES.txt
templates/_helpers.tpl
templates/deployment.yaml
templates/hpa.yaml
templates/httproute.yaml
templates/ingress.yaml
templates/service.yaml
templates/serviceaccount.yaml
templates/tests/
templates/tests/test-connection.yaml

sent 19,372 bytes received 302 bytes 39,348.00 bytes/sec
total size is 18,371 speedup is 0.93

# 进入到代码的项目目录中,提交代码到gitlab
[root@k8s-master01 chart-cicd]# cd go-project/
[root@k8s-master01 go-project]# git add .
[root@k8s-master01 go-project]# git commit -m '添加chart模板'
Author identity unknown

*** Please tell me who you are.

Run

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'root@k8s-master01.(none)')
[root@k8s-master01 go-project]# git config --global user.email "liujunwie@jenkins.com"
[root@k8s-master01 go-project]# git config --global user.name "liujunwei"
[root@k8s-master01 go-project]# git commit -m '添加chart模板'
[master 9349759] 添加chart模板
12 files changed, 550 insertions(+)
create mode 100644 chart/.helmignore
create mode 100644 chart/Chart.yaml
create mode 100644 chart/templates/NOTES.txt
create mode 100644 chart/templates/_helpers.tpl
create mode 100644 chart/templates/deployment.yaml
create mode 100644 chart/templates/hpa.yaml
create mode 100644 chart/templates/httproute.yaml
create mode 100644 chart/templates/ingress.yaml
create mode 100644 chart/templates/service.yaml
create mode 100644 chart/templates/serviceaccount.yaml
create mode 100644 chart/templates/tests/test-connection.yaml
create mode 100644 chart/values.yaml
[root@k8s-master01 go-project]# git push origin master
Username for 'http://192.168.0.222': root
Password for 'http://root@192.168.0.222':
Enumerating objects: 18, done.
Counting objects: 100% (18/18), done.
Delta compression using up to 4 threads
Compressing objects: 100% (16/16), done.
Writing objects: 100% (17/17), 7.07 KiB | 1.77 MiB/s, done.
Total 17 (delta 2), reused 0 (delta 0), pack-reused 0
To http://192.168.0.222/kubernetes/go-project.git
05efde4..9349759 master -> master

如下图所示,已经将添加的chart模板内容推送到gitlab的项目仓库

修改流水线的Jenkinsfile,集成helm进行发版,主要修改发版镜像以及发版的指令:

Jenkinsfile修改后的完整内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
pipeline {
agent {
kubernetes {
cloud 'kubernetes-study'
slaveConnectTimeout 1200
workspaceVolume persistentVolumeClaimWorkspaceVolume(claimName: "golang-pipeline", readOnly: false)
yaml '''
kind: Pod
apiVersion: v1
spec:
restartPolicy: Never
securityContext:
runAsUser: 0
fsGroup: 0
# nodeSelector:
# build: "true"
containers:
# jnlp容器,负责和Jenkins主节点通信,该容器中的配置不需要修改
- name: jnlp
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/jnlp-agent-docker:latest
args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
imagePullPolicy: IfNotPresent
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
# build容器,包含执行构建的命令,比如java的需要mvn构建,就可以用一个maven的镜像
- name: build #定义容器的名字,流水线的stage会用到改名字
# 使用golang镜像编译,版本根据自己的项目决定
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/golang:latest
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: golang-cache
mountPath: /go/pkg
readOnly: false
- name: localtime
mountPath: /etc/localtime
# kaniko 用来制作镜像的容器,可以是docker 或者其他可以用于构建镜像的工具
- name: kaniko
# 是示例中采用kaniko制作镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/kaniko-executor:debug
imagePullPolicy: IfNotPresent
command:
- "sleep"
args:
- "99d"
tty: true
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
- name: docker-registry-config #挂载docker配置文件,用于kaniko连接镜像仓库
mountPath: /kaniko/.docker
readOnly: false
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
# 定义发版的镜像,因为最终是发版至 Kubernetes集群,所以需要有一个 kubectl 命令或者是其他的发版工具也可以,比如 helm,根据实际情况配置自己的发版镜像
- name: kubectl #这里还用kubectl作为容器的名字,只是镜像换成了helm
# 发版的镜像根据实际需求进行变更,例如使用helm的镜像
image: registry.cn-beijing.aliyuncs.com/k8s-liujunwei/helm:4.0.4
imagePullPolicy: IfNotPresent
tty: true
command:
- "cat"
env:
- name: "LANGUAGE"
value: "en_US:en"
- name: "LC_ALL"
value: "en_US.UTF-8"
- name: "LANG"
value: "en_US.UTF-8"
volumeMounts:
- name: localtime
mountPath: /etc/localtime
readOnly: false
volumes:
- name: localtime
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
# 构建缓存的pvc
- name: golang-cache
persistentVolumeClaim:
claimName: golang-global-cache
readOnly: false
#docker认证信息的volume
- name: docker-registry-config
configMap:
name: docker-registry-config '''
}
}

//定义一些全局环境变量
environment {
COMMIT_ID = ""
HARBOR_ADDRESS = "192.168.0.89" //Harbor 地址
REGISTRY_DIR = "kubernetes" //Harbor 的项目目录
IMAGE_NAME = "go-project" //镜像的名称
NAMESPACE = "kubernetes" //该应用在 Kubernetes 中的命名空间
TAG = "" //镜像的 Tag,在此用 BUILD_TAG+COMMIT_ID 组成
GIT_URL = "git@192.168.0.222:kubernetes/go-project.git" //代码仓库地址
}
parameters {
gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue:'', description: 'Branch for build and deploy', name: 'BRANCH',quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE',tagFilter: '*', type: 'PT_BRANCH')
}
// 接下来是拉代码的 stage,这个 stage 是一个并行的 stage,因为考虑了该流水线是手动触发还是触发
stages {
stage('拉取代码') {
steps {
script {
def branchToUse = env.gitlabBranch ?: params.BRANCH
// 加一个保护,防止两个来源都为空
if (!branchToUse) {
error "未指定构建分支,请检查触发方式或参数配置"
}
git(
url: "${GIT_URL}",
branch: branchToUse,
changelog: true,
poll: true,
credentialsId: 'gitlab-key'
)
COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
TAG = "${env.BUILD_TAG}-${COMMIT_ID}"
echo "触发方式: ${env.gitlabBranch ? 'GitLab Webhook' : '手动触发'}"
echo "Branch: ${branchToUse}, Commit: ${COMMIT_ID}, Tag: ${TAG}"
}
}
}
stage('Building') {
steps {
container(name: 'build') {
sh """
cd helloserver
export GO111MODULE=on
export CGO_ENABLED=0
export GOPROXY=https://goproxy.cn,direct
go build -v -o app
echo '✅ Go 构建完成!'
ls -lh app
"""
}
}
}
stage('Build for creating image') {
steps {
container(name: 'kaniko') {
sh """
executor -d ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -c . --insecure --skip-tls-verify
"""
}
}
}
stage('部署到k8s') {
environment {
MY_KUBECONFIG = credentials('STUD_KUBECONFIG')
}
steps {
//因为我还是使用的kubectl作为容器的名字,只是镜像用的helm,所以这里还是引用kubectl,如果容器名字改了这里也要与之匹配
container(name: 'kubectl') {
sh """
# 原来的kubectl发版命令注释掉用helm指令进行发版
# kubectl --kubeconfig $MY_KUBECONFIG set image deployment/${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
# 使用helm发版时用下面helm指令
cd chart
helm upgrade \
--kubeconfig $MY_KUBECONFIG \
--install $IMAGE_NAME . \
--namespace ${NAMESPACE} \
--create-namespace \
--set fullnameOverride=$IMAGE_NAME \
--set nameOverride=$IMAGE_NAME \
--set replicaCount=1 \
--set image.repository=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME} \
--set image.tag=${TAG}
"""
}
}
}
}
}

修改文成后保存提交,接下来在Jenkins上创建一个任务测试流水线是否正常发布任务

流水线任务可以直接复制上述笔记中11.6步骤中的任务,这里就不进行演示了,构建任务之前先将之前手动创建的go-project项目的资源进行删除:

1
2
3
4
5
# 删除手动创建的go-project资源
[root@k8s-master01 go-project]# kubectl delete service -n kubernetes go-project
service "go-project" deleted
[root@k8s-master01 go-project]# kubectl delete deployments.apps -n kubernetes go-project
deployment.apps "go-project" deleted

构建任务Build with Parameters,根据需要选择分支

流水线日志提示成功

检查服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看pod的镜像
[root@k8s-master01 go-project]# kubectl get po -n kubernetes
NAME READY STATUS RESTARTS AGE
go-project-9d8856c8-vvhqr 1/1 Running 0 2m24s
spring-boot-project-756bd77956-pfnmj 1/1 Running 7 (27h ago) 68d
vue-project-7f4b645b5d-pd726 1/1 Running 7 (35h ago) 67d


#镜像已经是helm的镜像了
[root@k8s-master01 go-project]# kubectl get po -n kubernetes go-project-9d8856c8-vvhqr -oyaml|grep image
- image: 192.168.0.89/kubernetes/go-project:jenkins-go-project-helm-3-8d2b131
imagePullPolicy: IfNotPresent
imagePullSecrets:
image: 192.168.0.89/kubernetes/go-project:jenkins-go-project-helm-3-8d2b131
imageID: 192.168.0.89/kubernetes/go-project@sha256:66b272dc695881b720bccdaeaae80ed8ee3b0614535eceff3b8da72088c0768f

#检查ingress
[root@k8s-master01 go-project]# kubectl get ingress -n kubernetes
NAME CLASS HOSTS ADDRESS PORTS AGE
go-project nginx chart.test.com 192.168.0.84,192.168.0.85,192.168.0.86 80 17m

浏览器访问chart.test.com测试,记得要先添加hosts解析,如下图成功访问到项目页面

总结:在实际的工作中流水线可以按照9,10,11章中的步骤提前创建好相应的k8s资源后再用Jenkins进行发版,也可以使用14章中的方式进行发版,当然也可以在生产环境中用,但是在生产环境中还是建议直接使用测试好的镜像,而不是在生产环境没法发版的时候都要拉代码构建发版,建议生产上的发版使用13章中的方式,直接获取镜像进行发版。