15、DevOps

十五、Devops实践

1. 业务上线流程

应用部署或者更新都是采用手工的方式,但在企业内部,应用架构一般都采用微服务,大部分项目都会对应几十、上百,甚至上千个微服务,并且还不仅仅只有一个项目,所以采用手工方式上线是不太现实的事情,特别是随着应用的增多,对应的工作量也变得不可想象。由于上线的过程一般比较固定,大都是提前规定好、比较一致的流程,因此采用工具来完成这类“死板”的工作是比较推荐的方式,同时也能减少手工操作带来故障的风险。

目前更多的是在生产环境中持续集成(Continuous Integration,CI)与持续部署(Continuous Deployment,CD)的使用——实现Jenkins 流水线脚本自动发布应用到 Kubernetes 集群中,当然 CI/CD 是 DevOps 中非常重要的一个环节。

2. CI/CD管道

2.1 CI/CD简介

CI/CD是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD的核心概念是持续集成、持续交付(Continuous Delivery,CD)和持续部署。

CI/CD在整个应用生命周期内(从集成和测试阶段到交付和部署)引入了持续自动化和持续监控,这些关联的事务通常被称为“CI/CD管道”,由开发和运维团队以敏捷方式协同支持。

2.2 持续集成(CI)

目标:确保团队成员的代码变更能够快速有效地合并到主干,并及时发现和解决代码集成问题。

持续集成可以帮助开发人员更加频繁地将代码更改合并到共享分支或主干中。一旦开发人员对应用所做的更改被合并,系统就会通过自动构建应用并运行不同级别的自动化测试(通常是单元测试和集成测试)来验证这些更改,确保更改没有对应用造成破坏。这意味着测试内容涵盖了从类和函数到构成整个应用的不同模块,如果自动化测试发现新代码和现有代码之间有冲突,持续集成可以更加轻松快速地修复这些错误。

2.3 持续交付(CD)

目标:完成持续集成中构建单元测试和集成测试的自动化流程后,通过持续交付可以自动将已验证的代码发布到存储库。

为了实现高效的持续交付流程,务必要确保持续交付已内置于开发管道。持续交付的目标是拥有一个可随时部署到生产环境的代码库。在持续交付中,每个阶段(从代码更改的合并到生产就绪型构建版本的交付)都涉及测试自动化和代码发布自动化。在流程结束时,运维团队可以快速、轻松地将应用部署到生产环境中。

2.4 持续部署(CD)

目标:自动将经过测试的应用程序发布到生产环境,实现快速、可靠的软件部署。

对于一个成熟的CI/CD管道来说,最后的阶段是持续部署。作为持续交付(自动将生产就绪型构建版本发布到代码存储库)的延伸,持续部署可以自动将应用发布到生产环境中。由于生产之前的管道阶段没有手动门控,因此持续部署在很大程度上都得依赖精心设计的测试自动化。

2.5 CI和CD的区别

  • 持续集成,它属于开发人员的自动化流程。成功的CI意味着应用代码的最新更改会定期构建、测试并合并到共享存储中。该解决方案可以解决在一次开发中有太多应用分支,从而导致相互冲突的问题。
  • 持续交付通常是指开发人员对应用的更改会自动进行错误测试并上传到存储库(如GitLab或镜像仓库),然后由运维团队将其部署到实时生产环境中,旨在解决开发和运维团队之间可见性及沟通较差的问题,因此持续交付的目的就是确保尽可能减少部署新代码时所需的工作量。
  • 持续部署指的是自动将开发人员的更改从代码库发布到生产环境中以供客户使用,它主要为解决因手动流程降低应用交付速度,从而使运维团队超负荷的问题。持续部署以持续交付的优势为根基,实现了管道后续阶段的自动化。

3. 基于K8s的Devops平台设计

3.1 流程剖析

img

下面是基于Kubernetes和Jenkins搭建DevOps平台、如何在Kubernetes中发版、DevOps平台用到的工具的安装以及基于Kubernetes和Jenkins的CI/CD过程。

在Kubernetes中进行CI/CD的过程,一般的步骤如下:

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

Tips:中间还可能涉及自动化测试等其他步骤,可自行根据业务场景添加或删除。流水线步骤一般写在Jenkinsfile中,Jenkins会自动读取该文件,同时Jenkinsfile和Dockerfile可一并和代码放置于GitLab中,可以和代码一样实现版本控制(单独配置也可以)。

3.2 安装Jenkins

3.2.1 安装docker

安装docker的一些依赖

yum install -y yum-utils device-mapper-persistent-data lvm2

也可以安装一下代码补全插件:yum install -y bash-completion bash-completion-extras

下载 repo 文件,并把软件仓库地址替换为镜像站

yum-config-manager --add-repo  https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' 
/etc/yum.repos.d/CentOS-Base.repo

安装docker-ce

yum install docker-ce

安装指定版本的docker-ce

#查找Docker-CE的版本
[root@jenkins ~]# yum list docker-ce.x86_64 --showduplicates | sort -r
Loading mirror speeds from cached hostfile
Loaded plugins: fastestmirror, langpacks
docker-ce.x86_64            3:23.0.3-1.el7                      docker-ce-stable
docker-ce.x86_64            3:20.10.0-3.el7                     docker-ce-stable
docker-ce.x86_64            18.06.3.ce-3.el7                    docker-ce-stable

#例如安装18.06.3.ce-3.el7这个版本
[root@jenkins ~]# yum -y install docker-ce-18.06.3.ce-3.el7

启动docker并设置开机自启

systemctl start docker
systemctl enable docker

配置镜像加速,编辑配置文件。

cat > /etc/docker/daemon.json << EOF
{
   "registry-mirrors": [
   "https://k313isum.mirror.aliyuncs.com"
  ]
}
EOF

配置内核转发功能

临时性:
 echo 1 > /proc/sys/net/ipv4/ip_forward
永久性:
 vim /etc/sysctl.conf
  net.ipv4.ip_forward = 1
  
 #立即生效
 sysctl -p

重启Docker

systemctl restart docker

3.2.2 启动Jenkins

创建 Jenkins 的数据目录,防止容器重启后数据丢失。

mkdir -p /data/jenkins_data
chmod -R 777 /data/jenkins_data
useradd jenkins -u 1001 -M -s /sbin/nologin
chown jenkins /data/jenkins_data/

#或者
mkdir -p /data/jenkins_data
chmod 777 /data/jenkins_data

启动 Jenkins,并配置管理员账号密码为 admin / admin123

docker run -d --name=jenkins \
--restart=always \
-e JENKINS_USERNAME=admin \
-e JENKINS_PASSWORD=admin123 \
-e JENKINS_HTTP_PORT_NUMBER=8080 \
-p 80:8080 -p 50000:50000 \
-v /data/jenkins_data:/bitnami/jenkins bitnami/jenkins:2.401.1-debian-11-r1

Tips:其中 8080 端口为 Jenkins Web 界面的端口,50000 是 jnlp 使用的端口,后期 Jenkins Slave 需要使用 50000 端口和 Jenkins 主节点通信。

查看Jenkins日志:

docker logs jenkins

#有出现以下信息即可
hudson.lifecycle.Lifecycle#onReady: Jenkins is fully up and running

之后通过Jenkins主机的IP即可访问Jenkins

image-20240510163533568

3.2.3 配置Jenkins

配置Jenkins插件源,Dashboard -> Manage Jenkins -> Plugins -> Advanced settings

https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

image-20240510163723927

配置中文,需要勾选重启Jenkins,这样才会完全生效中文。

image-20240510172809366

image-20240510163848998

设置时区为上海

image-20240510172847676

image-20240510164147784

3.2.4 下载插件

在Dashboard -> 系统管理 -> 插件管理 -> Advanced settings 进行搜索插件并安装!

Git
Git Parameter
Git Pipeline for Blue Ocean
GitLab
Credentials
Credentials Binding
Blue Ocean
Common API for Blue Ocean
REST API for Blue Ocean
Web for Blue Ocean
Blue Ocean Pipeline Editor
Blue Ocean Core JS
Pipeline SCM API for Blue Ocean
Dashboard for Blue Ocean
GitHub Pipeline for Blue Ocean
Build With Parameters
Dynamic Extended Choice Parameter
Extended Choice Parameter
List Git Branches Parameter
Pipeline
Pipeline implementation for Blue Ocean
Pipeline: GitHub
Delivery Pipeline
Kubernetes
Kubernetes CLI
Kubernetes Credentials
Image Tag Parameter
Active Choices

Tips:安装以上插件建议分批安装,如果遇到出错的插件,直接重启Jenkins再尝试重新安装!http://URL/restart,还不行,直接在Available plugins搜索插件,然后点击插件的链接进行下载,最后在Advanced settings里面进行导入即可!

image-20240510165142697

这个提示是建议进行URL配置(如果是通过域名访问就需要暴露50000端口)

image-20240510171044225

3.2.5 备份基础环境数据

安装完这么多插件和配置也挺费时,备份一下数据目录,方便以后直接docker挂载。

docker stop jenkins
tar zcvf jenkins_data.tar.gz -C /data/jenkins_data/ .

3.2.6 用户权限管理

3.2.6.1 开启权限全局安全设置

Dashboard > 系统管理 > 全局安全配置

img

3.2.6.2 创建角色

Dashboard > 系统管理 > Manage and Assign Roles,创建全局角色,分别添加一个root和user。

  • root为Administer权限,该权限允许修改系统级别的配置,也就是执行高度敏感的操作,例如挂载本地系统访问(这就赋予了基本的操作系统权限)
  • user为Read权限,读权限对于查看Jenkins的大部分页面是必需的。当你不希望未认证的用户看到Jenkins页面时,该权限就很有用: 从匿名用户中取消该权限,然后给受认证的用户赋予该权限。

img

Tips:user这个角色需要绑定全部下面的Read权限,是为了给所有用户绑定最基本的Jenkins访问权限。

如果不给后续用户绑定这个角色,会报错误:用户名 is missing the Overall/Read permission

在同一个页面往下看就能新增项目角色,一个是dev,一个是devops。这里可以理解为组的权限,到时候我们再添加用户赋予这个组权限。下面权限很多,可以自定义配置。其中Pattern是使用正则表达式绑定项目。

  • dev项目角色能操作以“dev-”名称开头的项目。
  • devops项目角色能操作以“devops-”名称开头的项目。

image-20240510175242717

Tips:记得保存!

3.2.6.3 创建用户

Dashboard > 系统管理 > 管理用户,添加5个用户。

  • zzb这个用户授予root全局角色权限
  • test01这个用户授予dev项目角色权限,同时也要拥有user全局角色权限
  • test02这个用户授予devops项目角色权限,同时也要拥有user全局角色权限

image-20240510174624869

3.2.6.4 为用户分配角色

Dashboard > 系统管理 > Manage and Assign Roles,为用户分配全局角色。

image-20240510174907129

在同一个页面往下看就能项目角色分配,勾选相应项目角色即可

image-20240510175355605

Tips:记得保存!

3.2.6.5 创建项目测试权限

以zzb管理员账户创建两个项目,分别为dev-test和devops-test

img

结果为:

  • test01、test02用户登录,只能看到dev-test项目
  • test03、test04用户登录,只能看到devops-test项目

3.3 安装Gitlab

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

3.3.1 安装并初始化Gitlab

#安装Gitlab所有依赖
yum install -y curl openssh-server postfix wget git

#安装包列表(清华源)
https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/

#本文安装版本
wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-12.0.3-ce.0.el7.x86_64.rpm -P /soft/ --no-check-certificate

#安装Gitlab
cd /soft/
yum localinstall -y /soft/gitlab-ce-12.0.3-ce.0.el7.x86_64.rpm

#常用命令
gitlab-ctl  status
gitlab-ctl  stop
gitlab-ctl  start

安装完就能看到以下界面,需要进行配置。

img

修改域名和时区

[root@gitlab ~]# grep -n ^external_url /etc/gitlab/gitlab.rb
13:external_url 'http://gitlab.zzb.com'

[root@gitlab ~]# grep -n ^gitlab_rails /etc/gitlab/gitlab.rb
49:gitlab_rails['time_zone'] = 'Asia/Shanghai'

执行初始化,时间较长,耐心等待。

gitlab-ctl reconfigure

img

初始化完毕后在宿主机上添加一条hosts

10.0.0.105 gitlab.zzb.com

访问测试,没问题,设置密码,用户名是root

image-20240510181355613

3.3.2 汉化、优化Gitlab

汉化补丁:https://gitlab.com/xhang/gitlab

#查询Gitlab版本
[root@gitlab ~]# rpm -qa | grep gitlab-ce
gitlab-ce-12.0.3-ce.0.el7.x86_64

#下载对应版本
wget https://gitlab.com/xhang/gitlab/-/archive/v12.0.3-zh/gitlab-v12.0.3-zh.tar.gz -P /soft/

#解压本查看汉化包版本
[root@gitlab ~]# tar xf /soft/gitlab-v12.0.3-zh.tar.gz -C /soft/
[root@gitlab ~]# cat /soft/gitlab-v12.0.3-zh/VERSION
12.0.3

#停止Gitlab服务
gitlab-ctl stop

#汉化包覆盖(两个提示忽略即可)
[root@gitlab ~]# \cp -r /soft/gitlab-v12.0.3-zh/* /opt/gitlab/embedded/service/gitlab-rails/
cp: cannot overwrite non-directory ‘/opt/gitlab/embedded/service/gitlab-rails/log’ with directory ‘/soft/gitlab-v12.0.3-zh/log’
cp: cannot overwrite non-directory ‘/opt/gitlab/embedded/service/gitlab-rails/tmp’ with directory ‘/soft/gitlab-v12.0.3-zh/tmp’

#关闭目前不使用的组件,默认都是true,修改为 false。
#修改Gitlab配置文件 /etc/gitlab/gitlab.rb
prometheus['enable'] = false
prometheus['monitor_kubernetes'] = false
alertmanager['enable'] = false
node_exporter['enable'] = false
redis_exporter['enable'] = false
postgres_exporter['enable'] = false
gitlab_monitor['enable'] = false
prometheus_monitoring['enable'] = false
grafana['enable'] = false

#一键修改命令如下
sed -i "s/# prometheus\['enable'\] = true/prometheus\['enable'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# prometheus\['monitor_kubernetes'\] = true/prometheus\['monitor_kubernetes'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# alertmanager\['enable'\] = true/alertmanager\['enable'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# node_exporter\['enable'\] = true/node_exporter\['enable'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# redis_exporter\['enable'\] = true/redis_exporter\['enable'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# postgres_exporter\['enable'\] = true/postgres_exporter\['enable'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# gitlab_monitor\['enable'\] = true/gitlab_monitor\['enable'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# prometheus_monitoring\['enable'\] = true/prometheus_monitoring\['enable'\] = false/g" /etc/gitlab/gitlab.rb
sed -i "s/# grafana\['enable'\] = true/grafana\['enable'\] = false/g" /etc/gitlab/gitlab.rb

#自动重新配置 GitLab 服务并重启受影响的组件, GitLab会自动重启更改配置了的服务。
gitlab-ctl reconfigure

#重启Gitlab服务
gitlab-ctl restart

进入之后配置Language即可

image-20240510185559129

3.3.3 Git无法自动补全修复

#新建一个git-completion.bash文件,复制配置文件内容到该文件。
https://git.kernel.org/pub/scm/git/git.git/tree/contrib/completion/git-completion.bash

#复制文件
cp git-completion.bash  ~/.git-completion.bash

#vim ~/.bashrc添加下面命令
source ~/.git-completion.bash

#重新加载即可,如果是远程连接的重新连接就生效了
source ~/.bashrc

3.3.4 Gitlab备份与恢复

3.3.4.1 备份配置

修改Gitlab配置文件

#修改备份文件
[root@gitlab ~]# grep '^g' /etc/gitlab/gitlab.rb
#是否可以指定备份目录
gitlab_rails['manage_backup_path'] = true
#定义备份目录
gitlab_rails['backup_path'] = "/var/opt/gitlab/backup"
#备份的压缩包权限
gitlab_rails['backup_archive_permissions'] = 0644
#备份保留多久
gitlab_rails['backup_keep_time'] = 604800

自动重新配置 GitLab 服务并重启受影响的组件, GitLab会自动重启更改配置了的服务。

gitlab-ctl reconfigure

gitlab-rake gitlab:env:info 查看Gitlab版本信息,备份时会用到!

3.3.4.2 手动备份
#GitLab 版本>=12.2
gitlab-backup create

##GitLab 版本<=12.1
gitlab-rake gitlab:backup:create

下面执行gitlab-rake gitlab:backup:create备份成功后的过程

image-20240510184503211

查看备份目录,已经成功备份。

image-20240510184530398

备份须知

gitlab.rbgitlab-secrets.json 是 GitLab 的配置文件,包含了一些敏感信息和重要的配置项。下面是它们的具体作用和备份的信息:

  1. gitlab.rb:
    • gitlab.rb 是 GitLab 的主要配置文件,包含了许多配置选项,如数据库连接信息、SMTP 邮件设置、外部 URL、备份设置等。
    • 这个文件中的敏感信息包括数据库密码、SMTP 邮箱密码等。如果不小心泄露或丢失了这些信息,可能会影响 GitLab 的正常运行和安全性。
    • 备份 gitlab.rb 文件可以确保在恢复或迁移 GitLab 实例时,能够准确地还原配置。
  1. gitlab-secrets.json:
    • gitlab-secrets.json 文件包含了一些重要的密钥和令牌,用于加密和认证,以及其他安全相关的设置。
    • 这些密钥包括加密会话令牌、访问令牌、连接 GitLab 到其他服务的密钥等。
    • 备份 gitlab-secrets.json 文件非常重要,因为对这些密钥的访问及其安全控制可以保护 GitLab 系统的机密数据和安全性。

通过定期备份 gitlab.rbgitlab-secrets.json 文件,可以确保在需要迁移、恢复或升级 GitLab 实例时,能够保留重要的配置和敏感信息,并确保系统的安全性。

3.3.4.3 自动备份

写一个脚本,每天凌晨三点进行备份。脚本内容如下:

[root@gitlab gitlab]# cat /server/scripts/backup-gitlab.sh
#!/bin/bash
#1.备份gitlab数据
gitlab-rake gitlab:backup:create

#2.备份gitlab配置和密码文件
tar zcf /backup/gitlab/gitlab-conf-$(date +%F).tar.gz /etc/gitlab

crontab -e写入定时任务

00 03 * * * sh /server/scripts/backup-gitlab.sh &>/dev/null
3.3.4.4 恢复
  • 备份目录和gitlab.rb中定义的备份目录必须一致
  • GitLab的版本和备份文件中的版本必须一致,否则还原时会报错。

测试的时候可以备份后把一些项目或者群组、用户删了,看会不会恢复。(生产环境请不要这样子做)

#1.停止写入类服务
gitlab-ctl stop unicorn
gitlab-ctl stop sidekiq

#2.gitlab-rake恢复 (不需要加tar)
gitlab-rake gitlab:backup:restore BACKUP=备份文件

#新版本:
gitlab-backup restore BACKUP=1688050719_2023_06_29_12.0.3
#旧版本
gitlab-rake gitlab:backup:restore BACKUP=1688050719_2023_06_29_12.0.3

img

在恢复数据库之前,我们将删除所有现有的表,以避免将来的升级问题。请注意,如果您GitLab数据库中的自定义表这些表和所有数据将已删除。(yes)

img

恢复进行完就进行重启服务并检查服务是否正常

#重启服务
gitlab-ctl restart

#查看服务是否正常
gitlab-rake gitlab:check SANITZE=true

3.3.5 小练习

可以创建一个测试项目,进行一些简单的测试。首先创建一个组:

image-20240510185835229

之后在该组下创建一个 Project:

image-20240510185920511

选择创建一个空的项目,输入项目名称,然后点击 Create project 即可:

image-20240510190033060

之后可以将 Jenkins 服务器的 key 导入到 GitLab,首先生成密钥(如有可以无需生成不然会覆盖之前的):

ssh-keygen -t rsa -C "YOUR_EMAIL@ADDRESS.COM"
cat ~/.ssh/id_rsa.pub

image-20240510190309953

image-20240510190432521

添加后就可以在 Jenkins 服务器拉取代码:

yum install git -y
git clone git@10.0.0.105:kubernetes/test-project.git

Cloning into 'test-project'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.

修改文件,然后提交测试:

[root@k8s-master01 test]# git clone git@10.0.0.105:kubernetes/test-project.git
Cloning into 'test-project'...
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 7 (delta 1), reused 0 (delta 0)
Receiving objects: 100% (7/7), done.
Resolving deltas: 100% (1/1), done.

[root@k8s-master01 test]# ls
test-project
[root@k8s-master01 test]# cd test-project/
[root@k8s-master01 test-project]# vim README.md  #修改README文件
[root@k8s-master01 test-project]# git add .
[root@k8s-master01 test-project]# git commit -am "add some infos"
[master c57a8ad] add some infos
 1 file changed, 1 insertion(+), 1 deletion(-)
 
[root@k8s-master01 test-project]# git push origin master  #新版本的可能是main分支
Counting objects: 5, done.
Writing objects: 100% (3/3), 265 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@10.0.0.105:kubernetes/test-project.git
   48121b8..c57a8ad  master -> master

image-20240510192428476

3.4 安装Harbor

3.4.1 安装Docker、Docker-Compose

Harbor的所有服务组件都是在Docker中部署的,所以官方安装使用Docker-compose快速部署,所以我们需要安装Docker、Docker-compose。由于Harbor是基于Docker Registry V2版本,所以就要求Docker版本不小于1.10.0,Docker-compose版本不小于1.6.0。

安装一些依赖

yum install -y yum-utils device-mapper-persistent-data lvm2

也可以安装一下代码补全插件:yum install -y bash-completion bash-completion-extras

下载 repo 文件,并把软件仓库地址替换为镜像站

yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sed -i 's+https://download.docker.com+https://mirrors.tuna.tsinghua.edu.cn/docker-ce+' /etc/yum.repos.d/docker-ce.repo

安装docker-ce

yum install docker-ce

安装指定版本的Docker-CE

#查找Docker-CE的版本
yum list docker-ce.x86_64 --showduplicates | sort -r
Loading mirror speeds from cached hostfile
Loaded plugins: fastestmirror, langpacks
docker-ce.x86_64            3:23.0.3-1.el7                      docker-ce-stable
docker-ce.x86_64            3:20.10.0-3.el7                     docker-ce-stable
docker-ce.x86_64            18.06.3.ce-3.el7                    docker-ce-stable

#例如安装18.06.3.ce-3.el7这个版本
yum -y install docker-ce-18.06.3.ce-3.el7

配置镜像加速,编辑配置文件。

mkdir /etc/docker/
cat > /etc/docker/daemon.json << EOF
{
   "registry-mirrors": [
   "https://k313isum.mirror.aliyuncs.com"
  ]
}
EOF

启动docker并设置开机自启

systemctl daemon-reload
systemctl start docker
systemctl enable docker

配置内核转发功能

临时性:
 echo 1 > /proc/sys/net/ipv4/ip_forward
永久性:
 vim /etc/sysctl.conf
  net.ipv4.ip_forward = 1
  
 #立即生效
 sysctl -p

在安装 Docker Compose 之前,请确保你的机器已经正确运行了 Docker,一般安装docker时就会把docker compose一起安装了,如果执行docker-compose -v没有出现信息,则需要下载。

可在github上下载最新版本 https://github.com/docker/compose/releases

下面以下载v2.17.2为例

#从github上下载
wget https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-linux-x86_64

#赋予执行权限
chmod +x docker-compose-linux-x86_64

#有则覆盖
mv docker-compose-linux-x86_64 /usr/bin/docker-compose

#检查docker-compose的版本的信息,当我们执行完下面命令后,如果 Docker Compose 输出了当前版本号,就表示我们的 Docker Compose 已经安装成功。
docker-compose -v

3.4.2 下载并配置Harbor

下载地址:https://github.com/goharbor/harbor/releases ,本次以v2.3.1版本为例,先下载harbor-offline-installer-v2.3.1.tgz到主机上。

#创建目录
mkdir /soft
#解压软件包
tar xf harbor-offline-installer-v2.3.1.tgz -C /soft/
#进入harbor目录
cd /soft/harbor

修改配置文件内容如下,主要是配置hostsname、http和harbor_admin_password,注销https的配置(不然需要有证书)。其他保持默认即可。

  • 检测80端口是否被占用:netstat -nlptu|grep 80,若使用其它端口,需修改端口号。
# egrep -v '#|^$' harbor.yml
hostname: 10.0.0.138
http:
  port: 5000
harbor_admin_password: 123456
database:
  password: root123
  max_idle_conns: 100
  max_open_conns: 900
data_volume: /data
trivy:
  ignore_unfixed: false
  skip_update: false
  insecure: false
jobservice:
  max_job_workers: 10
notification:
  webhook_job_max_retry: 10
chart:
  absolute_url: disabled
log:
  level: info
  local:
    rotate_count: 50
    rotate_size: 200M
    location: /var/log/harbor
_version: 2.3.0
proxy:
  http_proxy:
  https_proxy:
  no_proxy:
  components:
    - core
    - jobservice
    - trivy

在harbor目录下进行安装

./install.sh

安装成功后,在浏览器上访问10.0.0.138:5000即可访问到harbor。账号admin、密码为上面配置所配置的密码。

image-20240510214305528

配置开机自启动文件

cat > /usr/lib/systemd/system/harbor.service <<EOF
[Unit]
Description=Harbor
After=docker.service systemd-networkd.service systemd-resolved.service
Requires=docker.service
Documentation=http://github.com/vmware/harbor

[Service]
Type=simple
Restart=on-failure
RestartSec=5
#注意docker-compose和harbor的安装位置
ExecStart=/usr/bin/docker-compose -f  /soft/harbor/docker-compose.yml up
ExecStop=/usr/bin/docker-compose -f /soft/harbor/docker-compose.yml down

[Install]
WantedBy=multi-user.target
EOF

加载Systemd配置,并设置开机自启动。

#手动停止
/usr/bin/docker-compose -f /soft/harbor/docker-compose.yml down

#加载配置和设置开机自启
systemctl daemon-reload
systemctl enable harbor.service

#启动Harbor
systemctl start harbor.service

3.4.3 推送镜像到Harbor

image-20240510214514512

在docker01主机上进行拉取官方镜像,然后推送到Harbor本地仓库中。

#拉取官方镜像
docker pull nginx:1.15.2

#查看镜像
# docker images | grep nginx
nginx               1.15.2              c82521676580        5 years ago         109MB

默认推送是https,通过配置 insecure-registries,可以告诉 Docker 某个仓库是不安全的,并允许 Docker 客户端与之进行通信,即便仓库使用的是不受信任的证书。(默认为80,不加端口)

# cat /etc/docker/daemon.json
{
   "registry-mirrors": [
   "https://k313isum.mirror.aliyuncs.com"
  ],
   "insecure-registries": ["10.0.0.138:5000"] 
}

systemctl restart docker

进行docker login,才能有权限进行推送。

#格式:docker login Harbor地址 -u Harbor用户 -p Harbor用户密码
docker login 10.0.0.138:5000 -u admin -p Harbor12345

对镜像进行打上tag,并推送。

#打tag格式:docekr tag 镜像名称:标签 你的IP(域名):端口/项目名称/镜像名称:标签 
docker tag nginx:1.15.2 10.0.0.138:5000/library/nginx:1.15.2

#推送镜像格式:docker push 你的IP(域名):端口/项目名称/镜像名称:标签
docker push 10.0.0.138:5000/library/nginx:1.15.2

拉取镜像

#拉取镜像格式:docker pull 你的IP(域名):端口/项目名称/镜像名称:标签
docker pull 10.0.0.190/nginx/nginx:1.15.2

3.4.5:k8s的runtime是contained(大于1.23.* 版本)

如果 Kubernetes 集群采用的是 Containerd 作为的 Runtime,配置 insecure-registry 只需要在

Containerd 配置文件的 mirrors 下添加自己的镜像仓库地址即可:

# vim /etc/containerd/config.toml

image-20240510215320395

配置完成后,重启 Containerd,之后进行 pull 测试:

 systemctl restart containerd
ctr -n k8s.io image pull CHANGE_HERE_FOR_YOUR_HARBOR_ADDRESS/kubernetes/harbor-exporter:v2.3.2 --plain-http --user admin:Harbor12345	#如果是公开的仓库不需要密码

也可以参考该博客

4. 前期准备

4.1 环境清单

主机名 IP 说明
jenkins(k8s-master01) 10.0.0.104 jenkins可持续集成、构建的工具
gitlab (k8s-master02) 10.0.0.105 gitlab代码仓库
harbor 10.0.0.138 镜像仓库
k8s-master01 10.0.0.104 k8s Master节点
k8s-master02 10.0.0.105 k8s Master节点
k8s-master03 10.0.0.106 k8s Master节点
k8s-node01 10.0.0.107 k8s Node节点
k8s-node02 10.0.0.108 k8s Node节点
k8s-node03 10.0.0.109 k8s Node节点

4.2 Gitlab准备

4.2.1 创建组、用户

创建一个dev群组,该群组再建立三个用户,分部是Leader、开发人员、运维人员。

  • 名称:Leader、Username:leader-user
  • 名称:开发人员、Username:dev-user
  • 名称:运维人员、Username:ops-user

image-20240510221526263

img

img

image-20240510222051556

dev群组成员权限设置如下:

image-20240510222215395

4.2.2 创建项目、添加人员

使用Leader账号创建一个kubernetes-demo的项目,并添加开发人员和运维人员。

image-20240510234533673

image-20240510234732358

4.2.3 开发人员配置公钥测试提交代码

首先生成密钥(如有可以无需生成)

ssh-keygen -t rsa -C "YOUR_EMAIL@ADDRESS.COM"

开发人员登录Gitlab,将公钥的内容放在GitLab中即可。

cat ~/.ssh/id_rsa.pub

image-20240510224043822

image-20240510224600479

拉取项目并测试提交代码到仓库中,出现了 “不允许将代码推送到受保护的分支”

git clone git@10.0.0.105:leader-user/kubernetes-demo.git

image-20240510235651527

登录leader账号进行对项目中的保护分支配置权限,设置开发人员能够进行推送到 “受保护的分支“ 。

img

Tips:正确的设置是master分支一直受保护,开发人员一般提交代码都提交到dev分支或者其他分支,只能是项目Leader能够进行合并到master分支操作。

重新提交测试

image-20240511000558665

4.3 Harbor准备

4.3.1 创建项目、用户

创建用户

image-20240511001214945

创建项目

image-20240511001314364

将dev设置为项目的开发者

image-20240511001258089

4.3.3 配置可推送镜像

k8s集群中所有节点下进行配置,默认推送是https,通过配置 insecure-registries,可以告诉 Docker 某个仓库是不安全的,并允许 Docker 客户端与之进行通信,即便仓库使用的是不受信任的证书。(默认为80,不加端口)

# cat /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors": ["https://k313isum.mirror.aliyuncs.com"],
  "insecure-registries": ["10.0.0.138:5000"]
}

systemctl restart docker

进行docker login,才能有权限进行推送。

#格式:docker login Harbor地址 -u Harbor用户 -p Harbor用户密码
docker login 10.0.0.190 -u YinJay -p Huawei@123

4.4 Jenkins准备

在使用Jenkins、GitLab、Harbor、Kubernetes打造DevOps平台实现CI/CD时,需要涉及很多证书、账号和密码、公钥和私钥的配置,这些凭证需要放在一个统一的地方管理,并且能在整个流水线中使用,而Jenkins提供的Credentials插件可以满足以上要求,并且在流水线中也不会泄露相关的加密信息。所以Harbor的账号和密码、GitLab的私钥、Kubernetes的证书均使用Jenkins的Credentials管理。

4.4.1 配置Kubernetes证书

在安装和维护Kubernetes集群时,都是使用kubectl操作集群,而kubectl操作集群需要一个KUBECONFIG文件,我们使用Jenkins控制Kubernetes时,也是使用该文件,所以该文件需要放置在Credentials中,之后可以被Jenkins的Kubernetes插件使用。

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

[root@k8s-master01 ~]# env | grep KUBECONFIG
KUBECONFIG=/etc/kubernetes/admin.conf

添加凭据

image-20240511002201842

image-20240511002812790

4.4.2 配置Harbor账号和密码

image-20240511002959558

4.4.3 配置GitLab Key

将Jenkins服务器的公钥放在GitLab中,之后Jenkins就可以通过自己的私钥登录GitLab,然后下载和上传代码。在流水线的执行过程中,往往第一个过程就是下载代码,所以Jenkins的流水线需要有下载代码的权限。此时可以将Jenkins的私钥保存至Jenkins凭证中,之后在流水线中引用即可。

生成密钥对,将id_rsa.pub公钥内容添加到Gitlab(使用Giltlab管理员账户)

ssh-keygen	#如果有不需要重新生成
cat /root/.ssh/id_rsa.pub

image-20240510190432521

将Jenkins的私钥,id_rsa的内容添加到凭据。一般位于~/.ssh/id_rsa

image-20240511021639701

4.4.4 配置Agent

通常情况下,Jenkins Slave会通过Jenkins Master节点的50000端口与之通信,所以需要开启Agent的50000端口。

image-20240511004655186

4.4.5 配置Cloud

Pipeline采用的Agent为在Kubernetes集群中创建的Pod,所以Jenkins要控制Kubernetes集群创建Pod充当Jenkins的Slave。之前添加了Kubernetes的证书至Jenkins凭证,接下来在Jenkins中配置Kubernetes直接引用该证书即可

image-20240511010011265

image-20240512021003680

Tips:添加完Kubernetes后,在Jenkinsfile的Agent中就可以选择该集群作为创建Slave的集群。
多集群配置:如果想要添加多个集群,重复上述步骤即可。首先添加Kubernetes凭证,然后添加Cloud即可。

4.4.6 关闭Git Host Key Verification
image-20240511011224952
4.4.7 添加host解析到Gitlab

出现以下图片问题,使用的域名方式,同时是docker起的jenkins,无法解析Gitlab,添加一条host记录到jenkins容器中。

#格式:docker exec -u root -it 容器名称 /bin/sh -c "echo 'gitlab的IP gitlab的域名' >> /etc/hosts"

docker exec -u root -it jenkins /bin/sh -c "echo '10.0.0.105 gitlab.zzb.com' >> /etc/hosts"

image-20240511013141044

image-20240511021423855

5. Jenkins流水线

5.1 什么是流水线

Jenkins流水线是一套插件,它支持在Jenkins中实现和集成持续交付流水线(Continuous Delivery Pipeline)。流水线提供了一组可扩展的工具,用于通过Pipeline DSL将简单到复杂的交付流水线以代码的形势展现,类似于基础设施即代码。

持续交付流水线会经历一个复杂的过程:从版本控制、向用户和客户提交软件、软件的每次变更(提交代码到仓库)到软件发布(Release)。这个过程包括以一种可靠并可重复的方式构建软件,以及通过多个测试和部署阶段来开发构建好的软件(称为Build)。

Jenkins流水线的定义被写在一个文本文件中(一般为Jenkinsfile),该文件“定制”了整个构建软件的过程。Jenkinsfile也可以被提交到项目的代码仓库中,在Jenkins中可以直接引用。将持续交付流水线作为应用程序的一部分,像其他代码一样进行版本化和审查,这是流水线即代码的基础。

创建Jenkinsfile并提交到代码仓库中的好处如下:

  • 自动为所有分支创建流水线构建过程。
  • 在流水线上进行代码复查/迭代。
  • 对流水线进行审计跟踪。
  • 流水线的代码可以被项目的多个成员查看和编辑。
  • 可以对Jenkinsfile进行版本控制。

5.2 流水线种类

Jenkins流水线主要分为声明式和脚本式两种,包含pipeline(流水线)、node(节点)、stage(阶段)、step(步骤)等区块。

  • pipeline:是用户定义的一个持续交付(CD)流水线模型。流水线的代码定义了整个构建过程,包括构建、测试和交付应用程序的阶段。另外,pipeline块是声明式流水线语法的关键部分。
  • node:是一个机器,它是Jenkins环境的一部分,另外,node块是脚本化流水线语法的关键部分。
  • stage:块定义了在整个流水线的执行任务中概念不同的子集(比如Build、Test、Deploy阶段),它被许多插件用于可视化Jenkins流水线当前的状态/进展。
  • step:本质上是指通过一个单一的任务告诉Jenkins在特定的时间点需要做什么,比如要执行shell命令,可以使用sh SHELL_COMMAND。

5.2.1 声明式流水线

在声明式流水线的语法中,流水线过程定义在pipeline{}中,pipeline块定义了整个流水线中完成的所有工作,比如:

pipeline {
    agent any // 
    stages {
        stage('Build') { 
            steps {
                // 
            }
        }
        stage('Test') { 
            steps {
                // 
            }
        }
        stage('Deploy') { 
            steps {
                // 
            }
        }
    }
}

agent any:在任何可用的代理上执行流水线或它的任何阶段,也就是执行流水线过程的位置,也可以指定到具体的节点。

stage:定义流水线的执行过程(相当于一个阶段),比如上文所示的Build、Test、Deploy,但是这个名字是根据实际情况定义的,并非固定的名字。

steps:执行某阶段具体的步骤。

5.2.2 脚本式流水线

在脚本化流水线的语法中,会有一个或多个node块在整个流水线中执行核心工作,比如:

node {  
    stage('Build') { 
        // 
    }
    stage('Test') { 
        // 
    }
    stage('Deploy') { 
        // 
    }
}

node:在任何可用的代理上执行流水线或它的任何阶段,也可以指定到具体的节点。

stage:和声明式的含义一致,定义流水线的阶段。stage块在脚本化流水线语法中是可选的,然而在脚本化流水线中实现stage块,可以清楚地在Jenkins UI界面中显示每个stage的任务子集。

5.2.3 两者主要区别

  1. 声明式 Pipeline 会在执行前就会校验 Pipeline 语法是否正确,而脚本式不会。
  2. 如果某个 stage 执行失败,修复后声明式 Pipeline 可以直接跳到该 stage 重新执行,而脚本式要从头来过。
  3. ption指令用于配置整个jenkins pipeline本身的选项,根据具体的选项不同,可以将其放在pipeline块或者stage块中。虽然声明式pipeline 和脚本式 pipeline 都支持 options 选项,但声明式 options 和 pipeline 代码逻辑是分开的,而脚本式 options 和代码逻辑是嵌套在一起的,如果有多个options需要设置代码可读性差。

5.3 声明式Pipeline语法

声明式是Jenkins新一代的流水线编写方式,而且声明式流水线支持类似Blue Ocean这类的图形化工具进行在线编辑。

所有有效的声明式流水线必须包含在一个pipeline块中,比如以下是一个pipeline块的格式:

pipeline {

 }

在声明式流水线中,有效的基本语句和表达式遵循与Groovy的语法同样的规则,但有以下例外:

流水线顶层必须是一个block,即pipeline{}。

分隔符可以不需要分号,但是每条语句都必须在自己的行上。

块只能由sections、directives、steps或assignment statements组成。

属性引用语句被当作无参数的方法调用,比如input会被当作input()。

5.3.1 agent

agent表示整个流水线或特定阶段中的步骤和命令执行的位置,该部分必须在pipeline块的顶层被定义,也可以在stage中再次定义,但是stage级别是可选的。

pipeline {
    agent any
}

agent的配置函数

any:在任何可用的代理上执行流水线

none:表示该pipeline脚本没有全局的agent配置。当顶层的agent配置为none时,每个stage部分都需要包含它自己的agent。

pipeline {
    agent none
    stages {
        stage('Stage For Build'){
            agent any
        }
    }
}

label:选择某个具体的节点执行pipeline命令,例如agent { label 'my-defined-label' }。

pipeline {
    agent none
    stages {
        stage('Stage For Build'){
            agent { label 'my-defined-label' }
        }
    }
}

node:它和label配置类似,只不过可以添加一些额外的配置,比如customWorkspace。

#label:一个字符串,该标签用于运行流水线的位置。该选项对node、docker和dockerfile可用,node必须选择该选项
#customWorkspace:一个字符串,用于自定义工作区运行流水线或stage。它可以是相对路径,也可以是绝对路径,该选项对node、docker和dockerfile可用。比如:

agent{
    node{
       label 'my-defined-label'
       customWorkspace '/some/other/path'
  }
}

dockerfile:使用从源码中包含的Dockerfile所构建的容器执行流水线或stage。为了使用该选项,Jenkinsfile必须从多个分支流水线中加载,或者从pipeline from SCM(后面的章节会涉及)加载。

#如果配置的语法为agent {dockerfile true},那么将从源码的根目录下使用Dockerfile文件进行构建。
#如果Dockerfile文件在其他目录,则需要使用dir字段更改Dockerfile所在的目录,配置方法为agent { dockerfile { dir 'DockerfileDir'} }。
#如果构建镜像的Dockerfile名称不是Dockerfile,可以使用filename选项指定Dockerfile文件名。同时也可以使用additionalBuildArgs参数传递构建镜像的参数。


假如有一个项目需要使用Dockerfile类型的agent,并且Dockerfile在build目录,文件名为Dockerfile.build,构建时期望有一个version的参数。此时对应的agent写法如下:
agent {
    dockerfile {
        label 'my-defined-label'
        dir 'build'
        filename 'Dockerfile.build'
        additionalBuildArgs '--build-arg version=1.0.2'
    }
}

docker:相当于dockerfile,可以直接使用docker字段指定外部镜像,可以省去构建的时间。比如使用maven镜像进行打包,同时可以指定args:

agent{
    docker{
       label 'my-defined-label'
       image 'maven:3-alpine'
       args '-v /tmp:/tmp'
  }
}

kubernetes:Jenkins也支持使用Kubernetes创建Slave,也就是常说的动态Slave。后面构建均使用基于Kubernetes的流水线,Pod的模板定义在Kubernetes区域中。配置示例如下:

agent {
    kubernetes {
        label podlabel
         yaml """
kind: Pod
metadata:
  name: jenkins-agent
spec:
  containers:
  - name: kaniko
    image: gcr.io/kaniko-project/executor:debug
    imagePullPolicy: Always
    command:
    - /busybox/cat
    tty: true
    volumeMounts:
      - name: aws-secret
        mountPath: /root/.aws/
      - name: docker-registry-config
        mountPath: /kaniko/.docker
  restartPolicy: Never
  volumes:
    - name: aws-secret
      secret:
        secretName: aws-secret
    - name: docker-registry-config
      configMap:
        name: docker-registry-config
"""
}

配置示例

假设有一个Java项目,需要用mvn命令进行编译,此时可以使用maven的镜像作为agent。配置如下:

pipeline {
    agent { docker 'maven:3-alpine' }
    stages {
        stage('Example Build') {
            steps {
                sh 'mvn -B clean verify'
            }
        }
    }
}

在流水线顶层将agent定义为none,那么此时stage部分就必须包含它自己的agent部分。在stage('Example Build')部分使用maven:3-alpine执行该阶段的步骤,在stage('Example Test')部分使用openjdk:8-jre执行该阶段的步骤。此时Pipeline如下:

pipeline {
    agent none
    stages {
        stage('Example Build') {
            agent { docker 'maven:3-alpine' }
            steps {
                echo 'Hello, Maven'
                sh 'mvn --version'
            }
        }
        stage('Example Test') {
            agent { docker 'openjdk:8-jre' }
            steps {
                echo 'Hello, JDK'
                sh 'java -version'
            }
        }
    }
}

也可以用基于Kubernetes的agent实现。比如定义具有3个容器的Pod,分别为jnlp(负责和Jenkins Master通信)、build(负责执行构建命令)、kubectl(负责执行Kubernetes相关命令),在steps中可以通过containers字段选择在某个容器执行命令:

pipeline {
    agent {
        kubernetes {
            cloud 'kubernetes-default'
            slaveConnectTimeout 1200
            yaml '''
apiVersion: v1
kind: Pod
spec:
    containers:
      - args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
        image: 'registry.cn-beijing.aliyuncs.com/citools/jnlp:alpine'
        name: jnlp
        imagePullPolicy: IfNotPresent
      - command:
          - "cat"
        image: "registry.cn-beijing.aliyuncs.com/citools/maven:3.5.3"
        imagePullPolicy: "IfNotPresent"
        name: "build"
        tty: true
      - command:
          - "cat"
        image: "registry.cn-beijing.aliyuncs.com/citools/kubectl:self-1.17"
        imagePullPolicy: "IfNotPresent"
        name: "kubectl"
        tty: true
'''
    }
}
    stages {
        stage('Building') {
            steps {
                container(name: 'build') {
                sh """
                    mvm clean install
                   """
                }
            }
        }
        stage('Deploy') {
            steps {
            container(name: 'kubectl') {
                sh """
                    kubectl get node
                   """
                }
            }
        }
    }
}

5.3.2 Post

post一般用于流水线结束后的进一步处理,比如错误通知等。post可以针对流水线不同的结果做出不同的处理,就像开发程序的错误处理,比如Python语言的try catch。post可以定义在Pipeline或stage中,目前支持以下post-condition:

  • always:无论pipeline或stage的完成状态如何,都允许运行该post中定义的指令。
  • changed:只有当前pipeline或stage的完成状态与它之前的运行不同时,才允许在该post部分运行该步骤。
  • fixed:当本次pipeline或stage成功(success),且上一次构建失败(failure)或不稳定(unstable)时,允许运行该post中定义的指令。
  • regression:当本次pipeline或stage的状态为失败、不稳定或终止,且上一次构建的状态为成功时,允许运行该post中定义的指令。
  • failure:只有当前pipeline或stage的完成状态为失败,才允许在post部分运行该步骤,通常这时在Web界面中显示为红色。
  • success:当前状态为成功时,执行post步骤,通常在Web界面中显示为蓝色或绿色。
  • aborted:当前状态为终止(aborted)时,执行该post步骤,通常是由流水线被手动终止触发的,这时在Web界面中显示为灰色。
  • unsuccessful:当前状态不是成功时,执行该post步骤。
  • cleanup:无论pipeline或stage的完成状态如何,都允许运行该post中定义的指令。它和always的区别在于,cleanup会在其他post-condition执行之后执行。

一般情况下,post部分放在流水线的底部,比如本实例,无论stage的完成状态如何,都会输出一条“I will always say Hello again!”信息:

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
    post {
        always {
            echo 'I will always say Hello again!'
        }
    }
}

也可以将 post 写在 stage:

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                sh 'EXECUTE_TEST_COMMAND'
            }
            post {
                failure {
                    echo "Pipeline Testing failure..."
                }
            }
        }
    }
}

5.3.3 Stages

Stages 包含一个或多个 stage 指令,同时可以在 stage 中的 steps 块中定义真正执行的指令。比如创建一个流水线,stages 包含一个名为 Example 的 stage,该 stage 执行 echo 'Hello World' 命令输出 Hello World 字符串:

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World ${env.BUILD_ID}'
            }
        }
    }
}

5.3.4 Steps

Steps 部分在给定的 stage 指令中执行的一个或多个步骤,比如在 steps 定义执行一条 shell 命令:

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

或者是使用 sh 字段执行多条指令:

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                sh """
                    echo 'Execute building...'
                    mvn clean install
                   """
            }
        }
    }
}

5.3.5 environment

environment主要用于在流水线中配置一些环境变量,根据配置的位置决定环境变量的作用域。environment可以定义在pipeline中作为全局变量,也可以配置在stage中作为该stage的环境变量。

该指令支持一个特殊的方法credentials(),该方法可用于在Jenkins环境中通过标识符访问预定义的凭证。对于类型为Secret Text的凭证,credentials()可以将该Secret中的文本内容赋值给环境变量。对于类型为标准的账号密码型的凭证,指定的环境变量为username和password,并且也会定义两个额外的环境变量,分别为MYVARNAME_USR和MYVARNAME_PSW。

pipeline {
    agent any
    environment { // Pipeline 中定义,属于全局变量
        CC = 'clang'
    }
    stages {
        stage('Example') {
            environment { // 定义在 stage 中,属于局部变量
                AN_ACCESS_KEY = credentials('my-prefined-secret-text')
            }
            steps {
                sh 'printenv'
            }
        }
    }
}

5.3.6 options

Jenkins流水线支持很多内置指令,比如retry可以对失败的步骤重复执行n次,可以根据不同的指令实现不同的效果。比较常用的指令如下:

buildDiscarder:保留多少个流水线的构建记录,比如options {buildDiscarder(logRotator(numToKeepStr: '1')) }。

disableConcurrentBuilds:禁止流水线并行执行,防止并行流水线同时访问共享资源导致流水线失败,比如options { disableConcurrentBuilds() }。

disableResume:如果控制器重启,禁止流水线自动恢复,比如options {disableResume() }。

newContainerPerStage:agent为docker或dockerfile时,每个阶段将在同一个节点的新容器中运行,而不是所有的阶段都在同一个容器中运行,比如options {newContainerPerStage () }。

quietPeriod:流水线静默期,也就是触发流水线后等待一会再执行,比如options{ quietPeriod(30) }。

retry:流水线失败后的重试次数,比如options { retry(3) }。

timeout:设置流水线的超时时间,超过流水线时间,job会自动终止,比如options{ timeout(time: 1, unit: 'HOURS') }。单位:SECONDS(秒)、MINUTES(分)、HOURS(小时)。

skipDefaultCheckout:跳过checkout scm语句,比如options {skipDefaultCheckout () }。

timestamps:为控制台输出时间戳,比如options { timestamps() }。

配置示例如下,只需要添加options字段即可:

pipeline {
    agent any
    options {
        timeout(time: 1, unit: 'HOURS')
        timestamps()
    }
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

options除了写在pipeline顶层外,还可以写在stage中,但是写在stage中的options仅支持retry、timeout、timestamps,或者是和stage相关的声明式选项,比如skipDefaultCheckout。处于stage级别的options写法如下:

pipeline {
    agent any
    stages {
        stage('Example') {
            options {
                timeout(time: 3, unit: 'SECONDS')
            }
            steps {
                echo 'Hello World'
                sleep 10
            }
        }
    }
}

5.3.7 parameters

parameters提供了一个用户在触发流水线时应该提供的参数列表,这些用户指定参数的值可以通过params对象提供给流水线的step(步骤)。

目前支持的参数类型如下:

string:字符串类型的参数,例如parameters { string(name: 'DEPLOY_ENV',defaultValue:'staging', description: '') },表示定义一个名为DEPLOY_ENV的字符型变量,默认值为staging。

text:文本型参数,一般用于定义多行文本内容的变量,例如parameters {text(name:'DEPLOY_TEXT', defaultValue: 'One\nTwo\nThree\n', description:'') },表示定义一个名为DEPLOY_TEXT的变量,默认值是'One\nTwo\nThree\n'。

booleanParam:布尔型参数,例如parameters { booleanParam(name:'DEBUG_BUILD',defaultValue: true, description: '') }。

choice:选择型参数,一般用于给定几个可选的值,然后选择其中一个进行赋值,例如parameters { choice(name: 'CHOICES', choices: ['one', 'two', 'three'],description: '') },表示定义一个名为CHOICES的变量,可选的值为one、two、three。

password:密码型变量,一般用于定义敏感型变量,在Jenkins控制台会输出为*,例如parameters { password(name: 'PASSWORD', defaultValue: 'SECRET',description: 'A secret password') },表示定义一个名为PASSWORD的变量,其默认值为SECRET。

pipeline {
    agent any
    parameters {
        string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
        text(name: 'BIOGRAPHY', defaultValue: '', description: 'Enter someinformation about the person')
        booleanParam(name: 'TOGGLE', defaultValue: true, description: 'Toggle this value')
        choice(name: 'CHOICE', choices: ['One', 'Two', 'Three'], description: 'Pick something')
        password(name: 'PASSWORD', defaultValue: 'SECRET', description: 'Enter a password')
    }
    stages {
        stage('Example') {
            steps {
                echo "Hello ${params.PERSON}"
                echo "Biography: ${params.BIOGRAPHY}"
                echo "Toggle: ${params.TOGGLE}"
                echo "Choice: ${params.CHOICE}"
                echo "Password: ${params.PASSWORD}"
            }
        }
    }
}

5.3.8 triggers

在pipeline中可以用triggers实现自动触发流水线执行任务,可以通过Webhook、Cron、pollSCM和upstream等方式触发流水线。

假如某个流水线构建的时间比较长,或者某个流水线需要定期在某个时间段执行构建,可以使用cron配置触发器,比如周一到周五每隔4个小时执行一次:

pipeline {
    agent any
    triggers {
        cron('H */4 * * 1-5')
    }
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

cron('H */4 * * 1-5')的意思是:在工作日(星期一至星期五)的每4个小时的某个随机分钟(由H指定)运行Pipeline。

cron字段的写法和Linux Crontab类似,5个字段分别代表分、时、日、月、周,不同的是流水线中有一个“H”的写法。注意H并不是Hour,而是Hash的缩写,主要为了解决多个流水线在同一时间同时运行带来的系统负载压力。比如有十几个流水线都配置的是每天下午三点半执行,那么到了下午三点半,十几个流水线同时触发,难免会引来系统压力。此时可以使用H代替,H仍然会触发流水线,但是不会同时执行所有作业。

分钟(Minute) 小时(Hour) 日(Day of Month) 月(Month) 周(Day of Week)
0-59 0-23 1-31(取决于具体月份) 1-12 0-7(0和7是星期天)

要为一个字段指定多个值,可以使用以下运算符。按优先顺序排列:

  • * 指定所有有效值
  • M-N 指定一个范围值
  • M-N/X*/XX 为间隔数在M-N(指定范围)或*/X(整个范围)每次递增
  • A,B,…,Z 枚举多值

为了允许定期调度的任务在系统上产生均衡负载,应尽可能使用符号H(用于“hash”)。

示例:

  1. triggers{ cron('H/15 ') }(每15分钟执行一次)
  2. triggers{ cron('H(0-29)/10 ') }(每小时的前30分钟内每10分钟执行一次)
  3. triggers{ cron('45 9-16/2 1-5') }(从上午9:45开始每小时45分钟一次,每个工作日下午3:45结束)
  4. triggers{ cron('H H(9-16)/2 1-5') }(每个工作日上午9点至下午5点之间每两个小时一次)
  5. triggers{ cron('H H 1,15 1-11 *') }(除了12月之外,每个月的1日和15日每天一次)

使用cron字段可以定期执行流水线,如果代码更新,想要重新触发流水线,可以使用pollSCM字段:

pipeline {
    agent any
    triggers {
        pollSCM('H */4 * * 1-5')
}
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

除了上述触发器外,也支持upstream型的触发器。upstream可以根据上游job的执行结果决定是否触发该流水线。比如当job1或job2执行成功时触发该流水线:

pipeline {
    agent any
    triggers {
        upstream(upstreamProjects: 'job1,job2', threshold: hudson.model.Result.SUCCESS)
}
    stages {
        stage('Example') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

Tips:目前支持的状态有SUCCESS、UNSTABLE、FAILURE、NOT_BUILT、ABORTED等。

5.3.9 stage

stage指令位于stages下,包含一个steps、一个agent(可选)或其他特定的stage指令。流水线中实际执行的指令都在stage中配置,所以在流水线中,至少有一个stage。

pipeline {
    agent any
    stages {
        stage('Example') {
            steps {
                echo 'Hello World ${env.BUILD_ID}'
            }
        }
    }
}

5.3.10 input

input字段可以实现在流水线中进行交互式操作,比如选择要部署的环境、是否继续执行某个阶段等。配置input支持以下选项:

  • message:必选,需要用户进行input的提示信息,比如“是否发布到生产环境?”。
  • id:可选,input的标识符,默认为stage的名称。
  • ok:可选,确认按钮的显示信息,比如“确定”“允许”。
  • submitter:可选,允许提交input操作的用户或组的名称,如果为空,任何登录用户均可提交input。
  • submitterParameter:使用submitterParameter设置环境变量的值作为可提交input的用户。
  • parameters:提供一个参数列表供input使用。

假如需要配置一个提示消息为“还继续吗”、确认按钮为“继续”、提供一个PERSON变量的参数,并且只能由登录用户alice和bob提交的input流水线:

pipeline {
    agent any
    stages {
        stage('Example') {
            input {
                message "还继续么?"
                ok "继续"
                submitter "alice,bob"
                parameters {
                    string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
                }
            }
            steps {
                echo "Hello, ${PERSON}, nice to meet you."
            }
        }
    }
}

5.3.11 when

when指令允许流水线根据给定的条件决定是否应该执行该stage,when指令必须至少包含一个条件。如果when包含多个条件,所有的子条件必须都返回True,stage才能执行。when也可以结合not、allOf、anyOf语法达到更灵活的条件匹配。

目前比较常用的内置条件如下:

  • branch:当正在构建的分支与给定的分支匹配时,执行这个stage,例如when { branch'master' }。注意,branch只适用于多分支流水线。
  • changelog:匹配提交的changeLog决定是否构建,例如when {changelog'.*^\\[DEPENDENCY\\] .+$' }
  • environment:当指定的环境变量和给定的变量匹配时,执行这个stage,例如when{ environment name: 'DEPLOY_TO', value: 'production' }。
  • equals:当期望值和实际值相同时,执行这个stage,例如when { equals expected: 2, actual:currentBuild.number }。
  • expression:当指定的Groovy表达式评估为True时,执行这个stage,例如when { expression{ return params.DEBUG_BUILD } }。
  • tag:如果TAG_NAME的值和给定的条件匹配,执行这个stage,例如when {tag "release-*" };
  • not:当嵌套条件出现错误时,执行这个stage,必须包含一个条件,例如when { not { branch'master' } }。
  • allOf:当所有的嵌套条件都正确时,执行这个stage,必须包含至少一个条件,例如when{ allOf { branch 'master'; environment name: 'DEPLOY_TO', value:'production' } }。
  • anyOf:当至少有一个嵌套条件为True时,执行这个stage,例如when { anyOf { branch 'master';branch 'staging' } }。

示例 1:当分支为 production 时,执行 Example Deploy 步骤:

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

也可以同时配置多个条件,比如分支是 production,而且 DEPLOY_TO 变量的值为 production时,才执行 Example Deploy:

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
                environment name: 'DEPLOY_TO', value: 'production'
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

也可以使用 anyOf 进行匹配其中一个条件即可,比如分支为 production,DEPLOY_TO 为 production 或 staging 时执行 Deploy:

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                branch 'production'
                anyOf {
                    environment name: 'DEPLOY_TO', value: 'production'
                    environment name: 'DEPLOY_TO', value: 'staging'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

也可以使用 expression 进行正则匹配,比如当 BRANCH_NAME 为 production 或 staging,并且 DEPLOY_TO 为 production 或 staging 时才会执行 Example Deploy:

pipeline {
    agent any
    stages {
        stage('Example Build') {
            steps {
                echo 'Hello World'
            }
        }
        stage('Example Deploy') {
            when {
                expression { BRANCH_NAME ==~ /(production|staging)/ }
                anyOf {
                    environment name: 'DEPLOY_TO', value: 'production'
                    environment name: 'DEPLOY_TO', value: 'staging'
                }
            }
            steps {
                echo 'Deploying'
            }
        }
    }
}

默认情况下,如果定义了某个stage的agent,在进入该stage的agent后,该stage的when条件才会被评估,但是可以通过一些配置选项决定when条件在何时执行。比如在进入stage的agent前评估when,可以使用beforeAgent,当when为true时才进行该stage。

目前支持的前置条件如下:

  • beforeAgent:如果beforeAgent为true,则会先评估when条件。在when条件为true时,才会进入该stage阶段。
  • beforeInput:如果beforeInput为true,则会先评估when条件。在when条件为true时,才会进入input阶段。
  • beforeOptions:如果beforeInput为true,则会先评估when条件。在when条件为true时,才会进入options阶段。

Tips:beforeOptions的优先级大于beforeInput又大于beforeAgent。

5.3.12 parallel

在声明式流水线中使用parallel字段,即可很方便地实现并发构建,比如对分支A、B、C进行并行处理:

pipeline {
    agent any
	stages {
		stage('Non-Parallel Stage') {
			steps {
				echo 'This stage will be executed first.'
			}
	   }
		stage('Parallel Stage') {
			when {
				branch 'master'
			}
			failFast true
			parallel {
				stage('Branch A') {
					agent {
						label "for-branch-a"
					}
					steps {
						echo "On Branch A"
					}
				}
				stage('Branch B') {
					agent {
						label "for-branch-b"
					}
					steps {
						echo "On Branch B"
					}
				}
				stage('Branch C') {
					agent {
						label "for-branch-c"
					}
					stages {
						stage('Nested 1') {
							steps {
								echo "In stage Nested 1 within Branch C"
							}
						}
						stage('Nested 2') {
							steps {
								echo "In stage Nested 2 within Branch C"
							}
						}
					}
				}
			}
		}
	}
}

设置 failFast 为 true 表示并行流水线中任意一个 stage 出现错误,其它 stage 也会立即终止。

pipeline {
    agent any
	options {
	    parallelsAlwaysFailFast()
	}
	stages {
		stage('Non-Parallel Stage') {
			steps {
				echo 'This stage will be executed first.'
			}
	   }
		stage('Parallel Stage') {
			when {
				branch 'master'
			}
			failFast true
			parallel {
				stage('Branch A') {
					agent {
						label "for-branch-a"
					}
					steps {
						echo "On Branch A"
					}
				}
				stage('Branch B') {
					agent {
						label "for-branch-b"
					}
					steps {
						echo "On Branch B"
					}
				}
				stage('Branch C') {
					agent {
						label "for-branch-c"
					}
					stages {
						stage('Nested 1') {
							steps {
								echo "In stage Nested 1 within Branch C"
							}
						}
						stage('Nested 2') {
							steps {
								echo "In stage Nested 2 within Branch C"
							}
						}
					}
				}
			}
		}
	}
}

5.4 Jenkinsfile的使用

在 Web UI 或 Jenkinsfile 中定义流水线,不过通常将 Jenkinsfile 放置于代码仓库中(当然也可以放在单独的代码仓库中进行管理)。

创建一个Jenkinsfile并将其放置于代码仓库中,有以下好处:

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

5.4.1 环境变量

5.4.1.1 静态变量

Jenkins有许多内置变量可以直接在Jenkinsfile中使用,可以通过JENKINS_URL/pipeline-syntax/globals#env获取完整列表。目前比较常用的环境变量如下:

  • BUILD_ID:当前构建的ID,与Jenkins版本1.597+中的BUILD_NUMBER完全相同。
  • BUILD_NUMBER:当前构建的ID,和BUILD_ID一致。
  • BUILD_TAG:用来标识构建的版本号,格式为jenkins-${JOB_NAME}-${BUILD_NUMBER},可以对产物进行命名,比如生产的JAR包名字、镜像的TAG等。
  • BUILD_URL:本次构建的完整URL,比如http://buildserver/jenkins/job/MyJobName/17/
  • JOB_NAME:本次构建的项目名称。
  • NODE_NAME:当前构建节点的名称。
  • JENKINS_URL:Jenkins完整的URL,需要在System Configuration中设置。
  • WORKSPACE:执行构建的工作目录。

上述变量会保存在一个Map中,可以使用env.BUILD_ID或env.JENKINS_URL引用某个内置变量:

pipeline {
	agent any
	stages {
		stage('Example') {
			steps {
				echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
			}
		}
	}
}

除了上述默认的环境变量,也可以手动配置一些环境变量:

pipeline {
	agent any
	environment {
		CC = 'clang'
	}
	stages {
		stage('Example') {
			environment {
				DEBUG_FLAGS = '-g'
			}
			steps {
				sh 'printenv'
			}
		}
	}
}

上述配置了两个环境变量,一个 CC 值为 clang,另一个是 DEBUG_FLAGS 值为-g。但是两者定义的位置不一样,CC 位于顶层,适用于整个流水线,而 DEBUG_FLAGS 位于 stage 中,只适用于当前 stage。

5.4.1.2 动态变量

动态变量是根据某个指令的结果进行动态赋值,变量的值根据指令的执行结果而不同。如下所示:

pipeline {
	agent any
	environment {
		// 使用 returnStdout
		CC = """${sh(
				returnStdout: true,
				script: 'echo "clang"'
			)}"""
		// 使用 returnStatus
		EXIT_STATUS = """${sh(
			returnStatus: true,
			script: 'exit 1'
			)}"""
	}
	stages {
		stage('Example') {
			environment {
				DEBUG_FLAGS = '-g'
			}
			steps {
				sh 'printenv'
			}
		}
	}
}

returnStdout:将命令的执行结果赋值给变量,比如上述的命令返回的是 clang,此时 CC 的值为“clang ”。注意后面多了一个空格,可以用.trim()将其删除;
returnStatus:将命令的执行状态赋值给变量,比如上述命令的执行状态为 1,此时 EXIT_STATUS 的值为 1。

5.4.2 凭证管理

相对于明文变量,Jenkins也可以处理密文数据,比如账号、密码等。为了安全起见,这些变量一般不会直接显示在Jenkins控制台,此时可以使用Jenkins凭证进行管理,这样就不会显示在控制台中。

Jenkins的声明式流水线语法有一个credentials()函数,它支持secret text(加密文本)、username(用户名)和password(密码)以及secret file(加密文件)等。接下来介绍一些常用的凭证处理方法。

5.4.2.1 加密文本

将两个Secret文本凭证分配给单独的环境变量来访问Amazon Web服务,需要提前创建这两个文件的credentials。Jenkinsfile文件的内容如下:

pipeline {
	agent {
		// Define agent details here
	}
	environment {
		AWS_ACCESS_KEY_ID = credentials('jenkins-aws-secret-key-id')
		AWS_SECRET_ACCESS_KEY = credentials('jenkins-aws-secret-access-key')
	}
	stages {
		stage('Example stage 1') {
			steps {
				//
			}
		}
		stage('Example stage 2') {
			steps {
				//
			}
		}
	}
}

上述示例定义了两个全局变量AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY,这两个变量引用的是credentials的两个加密文本,并且这两个变量均可以在stages直接引用(通过$AWS_SECRET_ACCESS_KEY$AWS_ACCESS_KEY_ID)。如果在steps中使用echo $AWS_ACCESS_KEY_ID,此时返回的是****,加密内容不会被显示出来。

5.4.2.2 用户名和密码

本示例用来演示credentials账号、密码的使用,比如使用一个公用账号访问Bitbucket、GitLab、Harbor等。假设已经配置完成了用户名和密码形式的credentials,凭证ID为jenkins-bitbucket-common-creds。可以用以下方式设置凭证环境变量(BITBUCKET_COMMON_CREDS名称可以自定义):

environment {
    BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
}

上述配置会自动生成3个环境变量:

  • BITBUCKET_COMMON_CREDS:包含一个以冒号分隔的用户名和密码,格式为username:password。
  • BITBUCKET_COMMON_CREDS_USR:仅包含用户名的附加变量。
  • BITBUCKET_COMMON_CREDS_PSW:仅包含密码的附加变量。

此时,调用用户名和密码的Jenkinsfile如下:

pipeline {
	agent {
		// Define agent details here
	}
	stages {
		stage('Example stage 1') {
			environment {
				BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
			}
			steps {
				//
			}
		}
		stage('Example stage 2') {
			steps {
				//
			}
		}
	}
}

此时环境变量的凭证仅作用于 stage 1,也可以配置在顶层对全局生效。

5.4.2.3 加密文件

需要加密保存的文件,也可以使用 credential,比如链接到 Kubernetes 集群的 kubeconfig 文件等。假如已经配置好了一个 kubeconfig 文件,此时可以在 Pipeline 中引用该文件:

pipeline {
	agent {
		// Define agent details here
	}
	environment {
		MY_KUBECONFIG = credentials('my-kubeconfig')
	}
	stages {
		stage('Example stage 1') {
			steps {
				sh("kubectl --kubeconfig $MY_KUBECONFIG get pods")
			}
		}
	}
}

更多其它类型的凭证可以参考:https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#handling-credentials。

5.4.3 参数处理

声明式流水线支持很多开箱即用的参数,可以让流水线接收不同的参数以达到不同的构建效果,在17.3.2节讲解的参数均可用在流水线中。在Jenkinsfile中指定的parameters会在Jenkins Web UI自动生成对应的参数列表,此时可以在Jenkins页面单击Build With Parameters来指定参数的值,这些参数可以通过params变量被成员访问。

假设在Jenkinsfile中配置了名为Greeting的字符串参数,可以通过${params.Greeting}访问该参数,比如:

pipeline {
	agent any
	parameters {
		string(name: 'Greeting', defaultValue: 'Hello', description: 'How should I greet the world?')
	}
	stages {
		stage('Example') {
			steps {
				echo "${params.Greeting} World!"
			}
		}
	}
}

5.4.4 使用多个代理

流水线允许在 Jenkins 环境中使用多个代理,这有助于更高级的用例,例如跨多个平台执行构建、测试等。

比如,在 Linux 和 Windows 系统的不同 agent 上进行测试:

pipeline {
	agent none
	stages {
		stage('Build') {
			agent any
			steps {
				checkout scm
				sh 'make'
				stash includes: '**/target/*.jar', name: 'app'
			}
		}
		stage('Test on Linux') {
			agent {
				label 'linux'
			}
			steps {
				unstash 'app'
				sh 'make check'
			}
			post {
				always {
					junit '**/target/*.xml'
				}
			}
		}
		stage('Test on Windows') {
			agent {
				label 'windows'
			}
			steps {
				unstash 'app'
				bat 'make check'
			}
			post {
				always {
					junit '**/target/*.xml'
				}
			}
		}
	}
}

6. 流水线自动化构建项目

6.1 构建Java应用前期准备

image-20240511144917578

image-20240511145038792

6.2 自动化构建Java应用

6.2.1 定义Kubernetes资源

该项目部署到k8s集群中同一个Namespace中,本示例以kubernetes命名空间为例,所以需要先创建这个命名空间。

kubectl create namespace kubernetes

由于使用的是私有仓库,因此也需要先配置拉取私有仓库镜像的密钥。

kubectl create secret docker-registry harborkey --docker-server=10.0.0.138:5000 --docker-username=dev --docker-password=Harbor12345 --docker-email=dev@163.com -n kubernetes

image-20240511150408594

书写该应用的Deployment,Service,Ingress资源清单。

[root@k8s-master01 auto-deploy]# vim spring-deploy.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: spring-boot-project
  name: spring-boot-project
  namespace: kubernetes
spec:
  replicas: 3
  selector:
    matchLabels:
      app: spring-boot-project
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: spring-boot-project
    spec:
      containers:
      - name: spring-boot-project
        image: 10.0.0.138:5000/llibrary/nginx
        imagePullPolicy: IfNotPresent
        ports:
        - name: web
          containerPort: 8761
          protocol: TCP
        env:
        - name: TZ
          value: Asia/Shanghai
        - name: LANG
          value: C.UTF-8

        livenessProbe:
          failureThreshold: 2
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 8761
          timeoutSeconds: 2

        readinessProbe:
          failureThreshold: 2
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 8761
          timeoutSeconds: 2

        resources:
          limits:
            cpu: 994m
            memory: 1170Mi
          requests:
            cpu: 10m
            memory: 55Mi

      dnsPolicy: ClusterFirst
      restartPolicy: Always
      imagePullSecrets:
      - name: harborkey

---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: spring-boot-project
  name: spring-boot-project
  namespace: kubernetes
spec:
  ports:
  - name: web
    port: 8761
    protocol: TCP
    targetPort: 8761
  selector:
    app: spring-boot-project
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: spring-boot-project
  namespace: kubernetes
spec:
  ingressClassName: nginx
  rules:
  - host: spring-boot-project.test.com
    http:
      paths:
      - backend:
          service:
            name: spring-boot-project
            port:
              number: 8761
        path: /
        pathType: ImplementationSpecific

创建该应用的Deployment,Service,Ingress资源。

[root@k8s-master01 auto-deploy]# pwd
/root/auto-deploy
[root@k8s-master01 auto-deploy]# kubectl create -f spring-deploy.yaml
deployment.apps/spring-boot-project created
service/spring-boot-project created
ingress.networking.k8s.io/spring-boot-project created

Tips:在实际使用时,Java一般是后台应用,不需要使用Ingress暴露出去,只需要配置Service即可。如果需要被集群外部的用户直接访问,需要配置Ingress,通过域名发布。

给Node01节点打上Label,因为构建编译的Pod要部署在这。

kubectl label nodes k8s-node01 build=true
6.2.2 定义Jenkinsfile

接下来再 GitLab 的源代码中添加 Jenkinsfile。首先点击代码首页的“+”号,添加文件。

Tips:Jenkins构建脚本通常是在项目的代码仓库中,以Jenkinsfile的形式存在

image-20240511150908951

pipeline {
  agent {
    kubernetes {
      cloud 'kubernetes-study'
      slaveConnectTimeout 1200
      workspaceVolume hostPathWorkspaceVolume(hostPath: "/opt/workspace", readOnly: false)
      yaml '''
apiVersion: v1
kind: Pod
spec:
  hostAliases:
    - ip: "10.0.0.105"
      hostnames:
      - "gitlab.zzb.com"
  containers:
    - name: jnlp
      image: '10.0.0.138:5000/kubernetes/inbound-agent:latest-jdk17'
      args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"
          readOnly: false

    - name: "build"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "10.0.0.138:5000/kubernetes/aliyun-maven:3.5.3"
      imagePullPolicy: "IfNotPresent"
      command:
        - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"
        - mountPath: "/root/.m2/"
          name: "cachedir"
          readOnly: false

    - name: "kubectl"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "10.0.0.138:5000/kubernetes/kubectl:self-1.17"
      imagePullPolicy: "IfNotPresent"
      command:
        - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"
          readOnly: false
 
    - name: "docker"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "10.0.0.138:5000/kubernetes/docker:19.03.9-git"
      imagePullPolicy: "IfNotPresent"
      command:
        - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"
          readOnly: false
        - mountPath: "/var/run/docker.sock"
          name: "dockersock"
          readOnly: false
  restartPolicy: "Never"
  nodeSelector:
    build: "true"
  securityContext: {}
  volumes:
    - hostPath:
        path: "/var/run/docker.sock"
      name: "dockersock"
    - hostPath:
        path: "/usr/share/zoneinfo/Asia/Shanghai"
      name: "localtime"
    - name: "cachedir"
      hostPath:
        path: "/opt/m2"
'''
    }
}
  stages {
    stage('Pulling Code') {
      parallel {
        stage('Pulling Code by Jenkins') {
          when {
            expression {
              env.gitlabBranch == null
            }

          }
          steps {
            git(changelog: true, poll: true, url: 'git@gitlab.zzb.com:dev/spring-boot-project.git', branch: "${BRANCH}", credentialsId: 'Jenkins-key')
            script {
              COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
              
            }

          }
        }

        stage('Pulling Code by trigger') {
          when {
            expression {
              env.gitlabBranch != null
            }

          }
          steps {
            git(url: 'git@gitlab.zzb.com:dev/spring-boot-project.git', branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'Jenkins-key')
            script {
              COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
            }

          }
        }

      }
    }

    stage('Building') {
      steps {
        container(name: 'build') {
            sh """ 
              mvn clean install -DskipTests
            """
        }
      }
    }

    stage('Docker build for creating image') {
      environment {
        HARBOR_USER = credentials('HARBOR_ACCOUNT')
    }
      steps {
        container(name: 'docker') {
          sh """
          echo ${HARBOR_USER_USR} ${HARBOR_USER_PSW} ${TAG}
          docker build -t ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} .
          docker login -u ${HARBOR_USER_USR} -p ${HARBOR_USER_PSW} ${HARBOR_ADDRESS}
          docker push ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG}
          """
        }
      }
    }

    stage('Deploying to K8s') {
      environment {
        MY_KUBECONFIG = credentials('Kubernetes-v1.23.17')
    }
      steps {
        container(name: 'kubectl'){
           sh """
           /usr/local/bin/kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
           """
        }
      }
    }

  }
  environment {
    COMMIT_ID = ""
    HARBOR_ADDRESS = "10.0.0.138:5000"
    REGISTRY_DIR = "kubernetes"
    IMAGE_NAME = "spring-boot-project"
    NAMESPACE = "kubernetes"
    TAG = ""
  }
  parameters {
    gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
  }
}

如下图进行添加,内容在图片下面。(记得提前拉好镜像,并修改Jenkinsfile,同时如果有参数不一样的也需修改 jdk版本要跟Jenkins内Java版本一致

image-20240511160345127

docker pull jenkins/inbound-agent:latest-jdk17
docker tag jenkins/inbound-agent:latest-jdk17 10.0.0.138:5000/kubernetes/inbound-agent:latest-jdk17 
docker push 10.0.0.138:5000/kubernetes/jnlp-slave:latest-jdk11
 
docker pull registry.cn-beijing.aliyuncs.com/citools/maven:3.5.3
docker tag registry.cn-beijing.aliyuncs.com/citools/maven:3.5.3 10.0.0.138:5000/kubernetes/maven:3.5.3
docker push 10.0.0.138:5000/kubernetes/maven:3.5.3

docker pull registry.cn-beijing.aliyuncs.com/citools/kubectl:self-1.17
docker tag registry.cn-beijing.aliyuncs.com/citools/kubectl:self-1.17 10.0.0.138:5000/kubernetes/kubectl:self-1.17
docker push 10.0.0.138:5000/kubernetes/kubectl:self-1.17


docker pull registry.cn-beijing.aliyuncs.com/citools/docker:19.03.9-git
docker tag registry.cn-beijing.aliyuncs.com/citools/docker:19.03.9-git 10.0.0.138:5000/kubernetes/docker:19.03.9-git 
docker push 10.0.0.138:5000/kubernetes/docker:19.03.9-git 

为了使Jenkins部署项目使更快,配置Maven的仓库为阿里云镜像

[root@docker test]# docker start maven
maven
[root@docker test]# docker cp maven:/usr/share/maven/conf/settings.xml .
Successfully copied 11.8kB to /root/test/.
[root@docker test]# ls
Dockerfile  settings.xml
vim settings.xm	 #添加配置
<mirror>
  <id>aliyun</id>
  <mirrorOf>central</mirrorOf>
  <name>aliyun</name>
  <url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>

image-20240512020004144

[root@docker test]# vim Dockerfile
FROM maven:3.5.3
COPY settings.xml /usr/share/maven/conf/settings.xml
[root@docker test]# docker build -t aliyun-maven:3.5.3 .
[root@docker test]# docker tag aliyun-maven:3.5.3 10.0.0.138:5000/kubernetes/aliyun-maven:3.5.3
[root@docker test]# docker push 10.0.0.138:5000/kubernetes/aliyun-maven:3.5.3
#然后将此镜像替换Jenkins中的maven镜像
6.2.3 定义Dockerfile

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

# 基础镜像可以按需修改,可以更改为公司自有镜像
FROM registry.cn-beijing.aliyuncs.com/dotbalo/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

image-20240511170005103

6.2.4 解释Jnekinsfile

以下代码为简略版

pipeline {
  agent {
  #定义使用kubernetes作为agent
    kubernetes {
    #选择之前配置的Cloud的名称
      cloud 'kubernetes-study'
      slaveConnectTimeout 1200
      #将workspace改成hostPath,因为配置了Label,Pod会在固定节点进行创建。如果有存储可用,也可用PVC的模式
      workspaceVolume hostPathWorkspaceVolume(hostPath: "/opt/workspace", readOnly: false)
      yaml '''
apiVersion: v1
kind: Pod
spec:
#注入host记录,因为CoreDNS中没有这条记录,无法进行拉取。
  hostAliases:
    - ip: "10.0.0.105"
      hostnames:
      - "gitlab.zzb.com"
  containers:
  #jnlp容器,和Jenkins Master节点通信
    - name: jnlp
      image: '10.0.0.138:5000/kubernetes/jnlp-slave:latest-jdk11'
      args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
      imagePullPolicy: IfNotPresent
      ...(略)...

  #build容器,包含执行构建的命令,比如java的需要的mvn构建,就可以用一个maven镜像。
    - name: "build"  #容器的名字,流水线的 stage 可以直接使用该名字
      image: "10.0.0.138:5000/kubernetes/maven:3.5.3"
      imagePullPolicy: "IfNotPresent"
      command:
        - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/root/.m2/"  #Pod单独创建了一个缓存的volume,将其挂载到了maven插件的缓存目录,默认是/root/.m2
          name: "cachedir"
          readOnly: false
      ...(略)...
      
  #发版容器,最终打包的镜像需要发版到K8s集群中,所以需要kubectl命令。
    - name: "kubectl"
      image: "10.0.0.138:5000/kubernetes/kubectl:self-1.17" #镜像的版本可以替换为其它的版本,也可以不进行替换,因为只执行set命令, 所以版本是兼容的。
      imagePullPolicy: "IfNotPresent"
	  command:
        - "cat" #占位符,为了让容器保持运行状态,等待其他命令或信号。
      tty: true #分配一个伪终端(TTY)给容器
      ...(略)...
  #用于生成镜像的容器,需要包含docker命令。
    - name: "docker"
      image: "10.0.0.138:5000/kubernetes/docker:19.03.9-git"
      imagePullPolicy: "IfNotPresent"
	  command:
        - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/var/run/docker.sock"  #由于容器没有启动docker服务,所以将宿主机的docker挂载至容器即可。
          name: "dockersock"
          readOnly: false
      ...(略)...
  
  restartPolicy: "Never"
  #固定节点部署,需在节点上打上该Label
  nodeSelector:
    build: "true"
  volumes:
      #宿主机的Docker
    - hostPath:
        path: "/var/run/docker.sock"
      name: "dockersock"
    - hostPath:
        path: "/usr/share/zoneinfo/Asia/Shanghai"
      name: "localtime"
      #缓存目录
    - name: "cachedir"
      hostPath:
        path: "/opt/m2"
'''
    }	
}

#流水线作业的开始
  stages {
  
    stage('Pulling Code') {
      #parallel对分支A、B进行并行处理
      parallel {
        stage('Pulling Code by Jenkins') {
        #假如env.gitlabBranch为空,则该流水线为手动触发,那么就会执行该stage ,如果不为空则会执行同级的另外一个stage。
          when {
            expression {
              env.gitlabBranch == null
            }

          }
          #这里使用的是git插件拉取代码,BRANCH变量取自于parameters。
          #credential为之前创建的Jnekins私钥,url为项目地址
          steps {
            git(changelog: true, poll: true, url: 'git@gitlab.zzb.com:root/spring-boot-project.git', branch: "${BRANCH}", credentialsId: 'Jenkins-key') 
            script {
            #定义一些变量用于生成镜像的Tag,下面COMMIT_ID为获取最近一次提交的Commit ID。
              COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
              #BUILD_TAG:用来标识构建的版本号,格式为`jenkins-${JOB_NAME}-${BUILD_NUMBER}`,可以对产物进行命名,比如生产的JAR包名字、镜像的TAG等。
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
              
            }

          }
        }

        stage('Pulling Code by trigger') {
        #假如env.gitlabBranch不为空,则该流水线为webhook触发,那么就会执行该stage ,此时BRANCH变量为空。
          when {
            expression {
              env.gitlabBranch != null
            }

          }
          steps {
           #以下配置和上述一致,只是此时branch取的值为env.gitlabBranch
            git(url: 'git@gitlab.zzb.com:root/spring-boot-project.git', branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'Jenkins-key')
            script {
              COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
            }

          }
        }

      }
    }
   #使用Pod模板里面的build容器进行构建
    stage('Building') {
      steps {
        #编译命令,需要根据自己项目的实际情况进行修改,可能会不一致。
        container(name: 'build') {
            sh """ 
              mvn clean install -DskipTests
            """
        }
      }
    }

    stage('Docker build for creating image') {
    #首先取出来Harbor的账号密码
      environment {
        HARBOR_USER = credentials('HARBOR_ACCOUNT')
    }
    #生成编译产物后,需要根据该产物生成对应的镜像,此时可以使用Pod模板的docker容器。
      steps {
      #指定docker容器执行build命令 ,Dockerfile也是放在代码仓库,和Jenkinsfile同级。
        container(name: 'docker') {
          sh """
          echo ${HARBOR_USER_USR} ${HARBOR_USER_PSW} ${TAG}
          docker build -t ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} .
          
          #登录Harbor,HARBOR_USER_USR和HARBOR_USER_PSW由上述environment生成。
          docker login -u ${HARBOR_USER_USR} -p ${HARBOR_USER_PSW} ${HARBOR_ADDRESS}
          
          #将镜像推送至镜像仓库
          docker push ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG}
          """
        }
      }
    }
    
    #最后一步就是将该镜像发版至Kubernetes集群中,此时使用的是包含kubectl命令的容器。
    stage('Deploying to K8s') {
    #获取连接Kubernetes集群证书
      environment {
        MY_KUBECONFIG = credentials('Kubernetes-v1.23.17')
    }
      steps {
      #指定使用kubectl容器
        container(name: 'kubectl'){
           sh """
           #直接set更改Deployment的镜像即可,运行下面命令后,Kubernetes会自动更新指定部署的容器镜像,并启动新的Pod来替换旧的Pod。
           /usr/local/bin/kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
           """
        }
      }
    }

  }
  #流水线运行之前首先解析environment、parameters,需要定义全局变量如下,如果无固定值的也需要先创建该变量。
  environment {
    COMMIT_ID = ""
    HARBOR_ADDRESS = "10.0.0.138:5000"       #Harbor地址
    REGISTRY_DIR = "kubernetes"             #Harbor项目目录
    IMAGE_NAME = "spring-boot-project"  #镜像的名称
    NAMESPACE = "kubernetes"            #该应用在kubernetes中的命名空间
    TAG = ""                            #镜像的Tag,在此用BUILD_TAG + COMMIT_ID组成
  }
  #首次不会显示,需要先运行一次流水线。该字段会在Jenkins页面生成一个选择分支的选项。选择的分支会赋值给$BRANCH变量。
  parameters {
    gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
  }
}
6.2.5 自动化流水线执行

创建Jenkins任务,选择流水线。

image-20240511160433188

选择从项目中进行导入jenkinsfile

image-20240511160537674

第一次不会有Build with Parameters,需要先构建一次。(立马点取消也行)

img

遇到下面情况就得查一下hostPath的权限,赋予777权限。

img

img

执行完成!

image-20240512014058648

查看deployment的情况,已进行滚动更新更替镜像。

image-20240512014303928

6.2.6 webhook钩子触发

在测试环境中,通常发版速度较快,也不用考虑是否影响生产业务。所以可以通过webhook钩子触发更新,当有人push代码到dev分支中,将进行自动触发流水线的进行,将新开发的版本进行交付,方便测试人员以及开发人员进行测试。

在spring-boot-project的项目中配置,勾选。

img

其余保持默认,就是当有人进行push到某分支或者合并到某分支时触发webhook钩子。然后点开高级,勾选Filter branches by name,填写触发或合并的分支。

img

img

返回Jenkins任务项目配置中的源码管理,点击高级,然后点击Generate生成Secret token,同时这个webhook URL也需要复制到Gitlab。

image-20240512021804310

image-20240512022248382

将刚从获取的Secret token和webhook URL填写,并在下面点增加web钩子后,返回Jenkins配置任务界面点保存即可。

image-20240512022050559

在Gitlab进行测试,出现successfully HTTP 200即可。

image-20240512022614398

image-20240512022541773

刚才填写的是dev分支,此时在dev分支上进提交一些新的东西,看是否会自动触发流水线的执行。

image-20240512023200761

image-20240512023244860

image-20240512023335434

image-20240512023435085

如果配置了域名,可以通过域名访问(测试域名需要配置 hosts)

6.3 构建Vue应用前期准备

示例项目可以从 https://gitee.com/dukuan/vue-project.git ,进行导入到Gitlab中

image-20240513132915168

image-20240513134113642

6.4 自动化构建Vue应用

6.4.1 定义Kubernetes资源

书写该应用的Deployment,Service,Ingress资源清单。

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: vue-project
  name: vue-project
  namespace: kubernetes
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vue-project
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: vue-project
    spec:
      containers:
      - name: vue-project
        image: 10.0.0.138/library/nginx
        env:
        - name: TZ
          value: Asia/Shanghai
        - name: LANG
          value: C.UTF-8
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
          name: web
          protocol: TCP

        livenessProbe:
          failureThreshold: 2
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 80
          timeoutSeconds: 2


        readinessProbe:
          failureThreshold: 2
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 80
          timeoutSeconds: 2

        resources:
          limits:
            cpu: 994m
            memory: 1170Mi
          requests:
            cpu: 10m
            memory: 55Mi

      dnsPolicy: ClusterFirst
      imagePullSecrets:
      - name: harborkey
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: vue-project
  name: vue-project
  namespace: kubernetes
spec:
  ports:
  - name: web
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: vue-project
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vue-project
  namespace: kubernetes
spec:
  rules:
  - host: vue-project.test.com
    http:
      paths:
      - backend:
          service:
            name: vue-project
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific

创建该应用的Deployment,Service,Ingress资源。

[root@k8s-master01 auto-deploy]# pwd
/root/auto-deploy

[root@k8s-master01 auto-deploy]# kubectl create -f  vue-deploy.yaml
deployment.apps/vue-project created
service/vue-project created
ingress.networking.k8s.io/vue-project created

6.4.2 定义Jnekinsfile

接下来再 GitLab 的源代码中添加 Jenkinsfile。首先点击代码首页的“+”号,添加文件。

Tips:Jenkins构建脚本通常是在项目的代码仓库中,以Jenkinsfile的形式存在

image-20240513135401987

如下图进行添加,内容在图片下面。(记得提前拉好镜像,并修改Jenkinsfile,同时如果有参数不一样的也需修改)

image-20240513142838812

pipeline {
  agent {
    kubernetes {
      cloud 'kubernetes-study'
      slaveConnectTimeout 1200
      workspaceVolume hostPathWorkspaceVolume(hostPath: "/opt/workspace", readOnly: false)
      yaml '''
apiVersion: v1
kind: Pod
spec:
  hostAliases:
    - ip: "10.0.0.105"
      hostnames:
      - "gitlab.zzb.com"
  containers:
    - name: jnlp
      image: '10.0.0.138:5000/kubernetes/inbound-agent:latest-jdk17'
      args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
      imagePullPolicy: IfNotPresent
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"
          readOnly: false

    - name: "build"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "10.0.0.138:5000/kubernetes/node:22-debian-12"
      imagePullPolicy: "IfNotPresent"
      command:
        - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"

    - name: "kubectl"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "10.0.0.138:5000/kubernetes/kubectl:self-1.17"
      imagePullPolicy: "IfNotPresent"
      command:
       - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"
          readOnly: false

    - name: "docker"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      image: "10.0.0.138:5000/kubernetes/docker:19.03.9-git"
      imagePullPolicy: "IfNotPresent"
      command:
        - "cat"
      tty: true
      volumeMounts:
        - mountPath: "/etc/localtime"
          name: "localtime"
          readOnly: false
        - mountPath: "/var/run/docker.sock"
          name: "dockersock"
          readOnly: false
  restartPolicy: "Never"
  nodeSelector:
    build: "true"
  securityContext: {}
  volumes:
    - hostPath:
        path: "/var/run/docker.sock"
      name: "dockersock"
    - hostPath:
        path: "/usr/share/zoneinfo/Asia/Shanghai"
      name: "localtime"
'''
    }
}
  stages {
    stage('Pulling Code') {
      parallel {
        stage('Pulling Code by Jenkins') {
          when {
            expression {
              env.gitlabBranch == null
            }

          }
          steps {
            git(changelog: true, poll: true, url: 'git@gitlab.zzb.com:dev/vue-project.git', branch: "${BRANCH}", credentialsId: 'Jenkins-key')
            script {
              COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${BRANCH}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
              
            }

          }
        }

        stage('Pulling Code by trigger') {
          when {
            expression {
              env.gitlabBranch != null
            }

          }
          steps {
            git(url: 'git@gitlab.zzb.com:dev/vue-project.git', branch: env.gitlabBranch, changelog: true, poll: true, credentialsId: 'Jenkins-key')
            script {
              COMMIT_ID = sh(returnStdout: true, script: "git log -n 1 --pretty=format:'%h'").trim()
              TAG = BUILD_TAG + '-' + COMMIT_ID
              println "Current branch is ${env.gitlabBranch}, Commit ID is ${COMMIT_ID}, Image TAG is ${TAG}"
            }

          }
        }

      }
    }

    stage('Building') {
      steps {
        container(name: 'build') {
            sh """ 
              npm config set registry https://registry.npmmirror.com
              npm install
              npm run build
            """
        }
      }
    }

    stage('Docker build for creating image') {
      environment {
        HARBOR_USER = credentials('HARBOR_ACCOUNT')
    }
      steps {
        container(name: 'docker') {
          sh """
          echo ${HARBOR_USER_USR} ${HARBOR_USER_PSW} ${TAG}
          docker build -t ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} .
          docker login -u ${HARBOR_USER_USR} -p ${HARBOR_USER_PSW} ${HARBOR_ADDRESS}
          docker push ${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG}
          """
        }
      }
    }

    stage('Deploying to K8s') {
      environment {
        MY_KUBECONFIG = credentials('Kubernetes-v1.23.17')
    }
      steps {
        container(name: 'kubectl'){
           sh """
           /usr/local/bin/kubectl --kubeconfig $MY_KUBECONFIG set image deploy -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${REGISTRY_DIR}/${IMAGE_NAME}:${TAG} -n $NAMESPACE
           """
        }
      }
    }

  }
  environment {
    COMMIT_ID = ""
    HARBOR_ADDRESS = "10.0.0.138:5000"
    REGISTRY_DIR = "kubernetes"
    IMAGE_NAME = "vue-project"
    NAMESPACE = "kubernetes"
    TAG = ""
  }
  parameters {
    gitParameter(branch: '', branchFilter: 'origin/(.*)', defaultValue: '', description: 'Branch for build and deploy', name: 'BRANCH', quickFilterEnabled: false, selectedValue: 'NONE', sortMode: 'NONE', tagFilter: '*', type: 'PT_BRANCH')
  }
}

6.4.3 定义Dockerfile

在执行流水线过程时,需要将代码的编译产物做成镜像。Dockerfile 主要写的是如何生成公司业务的镜像。而本次示例是 Vue 项目(前端应用构建后一般会在 dist 文件下产生 html 文件),只需要把打包好的静态资源放在nginx目录的镜像中即可。代码如下:

FROM 10.0.0.138:5000/library/nginx
COPY dist/* /usr/share/nginx/html/

image-20240513153451406

6.4.4 自动化流水线执行

创建Jenkins任务,选择流水线。

image-20240513144005828

选择从项目中进行导入jenkinsfile

image-20240513145241522

image-20240513153631957

image-20240513152951070

6.5 UAT及生产环境流水线设计

6.5.1 设计简述

之前的都包含构建过程,一般会用在首次提交代码,然后构建,并发布至第一个项目的环境中,一般是开发环境或者测试环境。这类环境采用这种模式没有问题,因为构建和产生镜像的步骤是必需的。

但是在其他环境,比如Sit、UAT、生产环境,其实是没有必要重复构建的,可以直接选择镜像进行发版,当然这种模式和代码本身的设计有关系,也就是要做到配置分离和构建与代码配置无关才行。如果项目符合此类条件,可以将第一次构建的镜像发布到任意环境,可以采用镜像的方式进行发版,也可以使用该流水线进行回滚操作。

6.5.2 一次构建多次部署

img

创建一个新的任务spring-boot-project-UAT,选择流水线。

image-20240513160157190

勾选页面上参数化构建过程,选择参数类型为Image Tag Parameter,填写名称、Image Name(项目/镜像)、Tag Filter Pattern(默认匹配所有,不筛选)、Default Tag填写latest拉取最新。

image-20240513161852673

image-20240513161756456

image-20240513162625144

书写Pipeline,然后保存即可。

image-20240513162451366

pipeline {
     agent {
    kubernetes {
      cloud 'kubernetes-study'
      slaveConnectTimeout 1200
      yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
    # 只需要配置jnlp和kubectl镜像即可
    - name: jnlp
      image: '10.0.0.138:5000/kubernetes/inbound-agent:latest-jdk17'
      imagePullPolicy: IfNotPresent
      args: [\'$(JENKINS_SECRET)\', \'$(JENKINS_NAME)\']
    - command:
        - "cat"
      env:
        - name: "LANGUAGE"
          value: "en_US:en"
        - name: "LC_ALL"
          value: "en_US.UTF-8"
        - name: "LANG"
          value: "en_US.UTF-8"
      name: "kubectl"    
      image: "10.0.0.138:5000/kubernetes/kubectl:self-1.17"
      imagePullPolicy: "IfNotPresent"
      tty: true
  restartPolicy: "Never"
'''
    }	
}

   stages {
      stage('Deploy') {
		environment {
			MY_KUBECONFIG = credentials('Kubernetes-v1.23.17')
		}
         steps {
		 container(name: 'kubectl'){
            sh """
               echo ${IMAGE_TAG} # 该变量即为前台选择的镜像
               kubectl --kubeconfig=${MY_KUBECONFIG} set image deployment -l app=${IMAGE_NAME} ${IMAGE_NAME}=${HARBOR_ADDRESS}/${IMAGE_TAG} -n ${NAMESPACE}
               kubectl --kubeconfig=${MY_KUBECONFIG} get pod  -l app=${IMAGE_NAME} -n ${NAMESPACE} -w
            """
         }
		}
      }
   }
   environment {
    HARBOR_ADDRESS = "10.0.0.138:5000"
    NAMESPACE = "kubernetes"
	IMAGE_NAME = "spring-boot-project"
    TAG = ""
  }
}

测试能否获取到Tag,进行发版测试。

image-20240513163259143

查看该deployment目前使用的镜像

image-20240513163425369

开始回滚到上一个版本镜像

image-20240513163659144

image-20240513163729686

Pipeline流水线执行完后查看该deployment目前使用的镜像

image-20240513163747187

注:本篇学习笔记内容参考杜宽的《云原生Kubernetes全栈架构师》,视频、资料文档等,大家可以多多支持!还有YinJayChen语雀k8s训练营“我为什么这么菜”知乎博主等资料文档,感谢无私奉献!

posted @   zzbao  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示