Jenkins

Jenkins是一个开源的持续集成的服务器,Jenkins开源帮助我们自动构建各类项目。Jenkins强大的插件式,使得Jenkins可以集成很多软件,可能帮助我们持续集成我们的工程项目。

一、什么是CI、CD

Devops也就是开发运维一体化,而随着Devops的兴起,出现了持续集成持续交付以及持续部署的新方法,传统的软件开发和交付方法正在迅速变得过时。从历史上看,在敏捷时代,大多数公司会每月、每季度、每两年甚至每年发布部署或发布软件。然而,在DevOps时代,每周、每天、甚至每天多次。当SaaS正在占领世界时,我们可以轻松地动态更新应用程序,而无需强迫客户下载新组件。很多时候,他们甚至都不会意识到正在发生变化。开发团队通过软件交付流水线(Pipeline)实现自动化,以缩短交付周期,大多数团队都有自动化流程来检查代码并部署到新环境。

持续集成的重点是将各个开发人员的工作集合到一个代码仓库中。通常,每天都要进行几次,主要目的是尽早发现集成错误,使团队更加紧密结合,更好地协作。

持续交付的目的是最小化部署或释放过程中固有的摩擦。它的实现通常能够将构建部署的每个步骤自动化,以便任何时刻能够安全地完成代码发布(理想情况下)。

持续部署是一种更高程度的自动化,无论何时对代码进行重大更改,都会自动进行构建/部署。

Jenkins就是来实现以上持续集成工作的服务器。

二、持续集成环境搭建

Jenkins流程图:

2.1 Gitlab代码托管服务器安装

Gitlab 是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的Web服务。

  1. 开启postfix服务(支持gitlab发信功能)
1
2
systemctl start postfix
systemctl enable postfix
  1. 配置gitlab的yum源
1
2
3
4
5
6
7
8
9
vim /etc/yum.repos.d/gitlab-cd.repo
[gitlab-ce]
name=Gitlab CE Repository
baseurl=https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el$releasever/
gpgcheck=0
enabled=1

yum clean all
yum makecache
  1. 安装gitlab
1
yum -y install gitlab-ce
  1. 修改gitlab配置文件
1
2
3
4
5
6
7
8
9
vim /etc/gitlab/gitlab.rb
#用户访问所使用的URL,域名或者IP地址
external_url 'http://192.168.88.133'
#时区
gitlab_rails['time_zone'] = 'Asia/Shanghai'
#启用SMTP邮箱功能
gitlab_rails['smtp_enable'] = 'ture'
#使用SSH协议拉取代码所使用的连接端口
gitlab_rails['gitlab_shell_ssh_port'] = '22'
  1. 刷新配置
1
gitlab-ctl reconfigure
  1. 启动服务
1
2
3
gitlab-ctl restart
gitlab-ctl status
......
  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
version: '3.8'
services:
gitlab:
container_name: gitlab
image: gitlab/gitlab-ce:latest
restart: always
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://192.168.88.30'
gitlab_rails['time_zone'] = 'Asia/Shanghai'
gitlab_rails['gitlab_shell_ssh_port'] = '22'
ports:
- '80:80'
- '23:22'
volumes:
- '/root/cicd/gitlab/config:/etc/gitlab'
- '/root/cicd/gitlab/logs:/var/log/gitlab'
- '/root/cicd/gitlab/data:/var/opt/gitlab'
  1. 进入容器查看默认用户名和修改密码
1
2
3
4
5
6
7
8
9
gitlab-rails console
# 查找root用户
u=User.where(id:1).first
# 修改密码
u.password='toortoor'
# 确认修改密码
u.password_confirmation='toortoor'
# 保存修改
u.save

2.2 Gitlab创建组、用户、项目

Gitlab创建组:

  1. 添加组

  1. 设置组名和权限

Gitlab创建用户:

  1. 添加用户

  1. 配置选项

  1. 将用户添加到项目组

Gitlab创建项目:

  1. 在指定组中添加项目

  1. 设置项目名称

2.5 项目上传到Gitlab

2.4 Jenkins安装

  1. 安装JDK
1
yum -y install java-1.8.0-openjdk*
  1. 放行端口
1
2
firewall-cmd --zone=public --add-port=8888/tcp --permanent
firewall-cmd --reload
  1. 添加Jenkins官方源并安装
1
2
3
wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
yum -y install jenkins
  1. 修改配置文件
1
2
3
vim /etc/sysconfig/jenkins
JENKINS_USER="root"
JENKINS_PORT="8888"
  1. 查看初始密码
1
2
cat /var/lib/jenkins/secrets/initialAdminPassword
......
  1. docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
version: '3.8'
services:
jenkins:
container_name: jenkins
image: jenkins:2.60.3
restart: always
ports:
- '8080:8080'
- '50000:50000'
volumes:
- '/root/cicd/jenkins/home:/var/jenkins_home'

2.5 Jenkins中文插件安装

  1. 由于Jenkins官方下载插件很慢,我们修改为国内Jenkins插件地址,jenkins -> Manage jenkins -> Manage Plugins

  1. 更换配置文件中的地址
1
2
3
4
5
cp /var/lib/jenkins/updates/default.json /var/lib/jenkins/updates/default.json.backup

sed -i 's/https:\/\/updates.jenkins.io\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' /var/lib/jenkins/updates/default.json && sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' /var/lib/jenkins/updates/default.json

systemctl restart jenkins
  1. 安装中文插件

2.6 Jenkins用户权限管理

  1. 由于Jenkins功能较为简介,都要靠安装插件来丰富体验,所以用户管理需要安装role-based插件

  1. 在Configure Global Security开启刚刚安装的插件

  1. 在Manage and Assign Roles中创建角色baserole并分配具体权限

  1. 用户没加入角色前是没有访问权限的,将用户加入到角色baserole即可,但只有部分权限

2.7 Jenkins安装凭证管理插件

  1. 安装Credential Binding插件,安装完就可以看到多了凭据的功能

2.8 Jenkins普通用户密码认证

  1. 为了Jenkins能够拉取gitlab的代码,需要安装git插件

1
yum -y install git
  1. 创建普通用户凭证,注意这里的用户是gitlab创建好的用户

  1. 创建项目添加普通用户凭证

  1. build now构建

  1. Jenkins主机上查看是否构建成功

2.9 Jenkins使用ssh免密认证

  1. 在Jenkins主机上生产公钥私钥
1
ssh-keygen -t rsa
  1. 在gitlab上添加公钥

  1. 在Jenkins上添加ssh凭证

  1. 创建项目添加ssh凭证

  1. build now构建项目后

2.10 Jenkins安装Maven

  1. 下载maven
1
2
3
4
wget https://mirrors.bfsu.edu.cn/apache/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
mkdir maven
tar -xf apache-maven-3.6.3-bin.tar.gz
cp apache-maven-3.6.3-bin.tar.gz/* maven/
  1. 配置环境变量
1
2
3
4
5
6
vim /etc/profile
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
export MAVEN_HOME=/root/maven
export PATH=$PATH:$JAVA_HOME/bin:/$MAVEN_HOME/bin
source /etc/profile
mvn -v
  1. 全局工具配置里新增jdk和maven

  1. 在系统配置里添加三个变量

  1. 修改maven配置文件
1
2
3
4
5
6
7
8
9
mkdir /root/repo
vim /root/maven/conf/settings.xml
<localRepository>/root/repo</localRepository>
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
  1. 测试maven构建项目

2.11 Tomcat安装和配置

  1. 安装jdk和tomcat
1
2
3
4
5
6
yum -y install java-1.8.0-openjdk*
wget https://downloads.apache.org/tomcat/tomcat-9/v9.0.43/bin/apache-tomcat-9.0.43.tar.gz
tar -xf apache-tomcat-9.0.43.tar.gz
mkdir /root/tomcat
mv apache-tomcat-9.0.43.tar.gz/* /root/tomcat
./root/tomcat/bin/startup.sh
  1. 添加tomcat管理用户
1
2
3
4
5
6
7
8
9
vim /root/tomcat/conf/tomcat-users.xml
<role rolename="tomcat"/>
<role rolename="role1"/>
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="admin-gui"/>
<role rolename="admin-script"/>
<role rolename="manager-status"/>
<user username="tomcat" password="toortoor" roles="manager-gui,manager-script,tomcat,admin-gui,admin-script"/>
1
2
3
4
5
vim /root/tomcat/webapps/manager/META-INF/context.xml
<!--
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
-->
  1. 测试

三、Jenkins构建项目

Jenkins构建的项目类型主要为以下三种:

  1. 自由风格软件项目(freestyle project)
  2. Maven项目(maven project)
  3. 流水线项目(pipeline project)

3.1 自由风格软件项目构建

  1. 要将项目部署到tomcat服务器上,需要安装deploy to container插件

  2. Jenkins -> 新建items

  1. 使用ssh免密登录来拉取代码

  1. 使用maven编译打包

  1. 部署

  1. 再次构建后可以回到tomcat查看是否部署成功

3.2 Maven项目构建

  1. 安装Maven Integration插件

  2. 创建maven项目

  1. 构建设置,其余设置都相同

  1. 构建后查看是否部署成功

3.3 Pipeline project简介

pipeline就是一套运行在Jenkins上的工作流框架,将独立运行的单个或多个节点的任务连接起来,实现复杂流程的编排和可视化的工作。

优点有:

  • 自动地为所有分支创建流水线构建过程并拉取请求。
  • 在流水线上代码复查/迭代 (以及剩余的源代码)。
  • 对流水线进行审计跟踪。
  • 该流水线的真正的源代码,可以被项目的多个成员查看和编辑。

3.4 Pipeline流水线项目构建

  1. 安装pipeline插件
  2. 新建流水线项目

声明式流水线

3.5 Jenkinsfile脚本文件

pipeline脚本内容放在Jenkins服务器不好管理,所以就在项目添加一个Jenkinsfile文件并推送到gitlab上,实现直接拉取执行。

  1. 在项目下添加一个Jenkinsfile,将pipeline内容复制进去并推送到gitlab上

  1. 在pipeline项目中修改为在gitlab拉取Jenkinsfile文件

四、Jenkins构建细节

4.1 Jenkins常用的构建触发器

Jenkins内置4种构建触发器:

  • 触发远程构建
  • 其它工程构建后触发
  • 定时构建
  • 轮询SCM

4.2 触发远程构建

  1. 在项目中设置触发器

  1. 在浏览器输入JENKINS_URL/job/pipeline-project/build?token=TOKEN_NAME即可触发

4.3 其它工程构建后触发

是指某一工程构建后才会触发构建,这里测试freestyle工程构建后触发pipeline构建

  1. 在pipeline配置构建后触发

  1. freestyle工程构建后就会触发pipeline构建

4.4 定时构建

五个*分别代表分时日月周,H/2则代表每分时日月周

4.5 轮询SCM

和定时构建一样是设置时间来进行构建,不过轮询只有在代码仓库发生变化后才会触发,相当于定时检查

注意:轮询会定时扫描仓库的代码,增大系统的开销,不建议使用

4.6 Gitlab Hook自动触发构建

SCM轮询是Jenkins服务器主动检测gitlab中的代码有没有发生变化,而gitlab的webhook可以实现代码变更后向Jenkins服务器主动发送构建请求,实现自动构建。

  1. 在Jenkins安装gitlab和gitlab hook插件

  1. 在gitlab配置中允许发出请求

  1. 在项目中添加第一步中的地址

  1. 配置Jenkins允许接受gitlab发送过来的请求

4.7 Jenkins参数化构建

前边实操演示的都是默认从master下拉取代码,如果要实现在其他分支拉取代码,就需要设置参数化构建。

  1. 在Jenkins的项目中添加参数,这里选择字符串参数

  1. 修改Jenkinsfile文件中拉取代码部分的master为变量,并推送到gitlab

  1. 在项目中添加一个v1分支

  1. 在Jenkins中拉取v1代码构建

4.8 配置邮件服务器发送构建结果

  1. 安装Email Extension插件

  2. 在Jenkins中配置

  1. 在项目中创建一个email.html文件,添加以下内容并推送到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
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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${PROJECT_NAME}-第${BUILD_NUMBER}次构建日志</title>
</head>

<body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4">
<table width="95%" cellpadding="0" cellspacing="0"
style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
<tr>
<td>(本邮件是程序自动下发的,请勿回复!)<br/></td>
</tr>
<tr>
<td><h2>
<font color="#0000FF">构建结果 - ${BUILD_STATUS}</font>
</h2></td>
</tr>
<tr>
<td><br />
<b><font color="#0B610B">构建信息</font></b>
<hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>项目名称 : ${PROJECT_NAME}</li>
<li>构建编号 : 第${BUILD_NUMBER}次构建</li>
<li>触发原因: ${CAUSE}</li>
<li>构建日志: <a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
<li>构建 Url : <a href="${BUILD_URL}">${BUILD_URL}</a></li>
<li>工作目录 : <a href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>
<li>项目 Url : <a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
</ul>
</td>
</tr>
<tr>
<td><b><font color="#0B610B">Changes Since Last Successful Build:</font></b>
<hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td>
<ul>
<li>历史变更记录 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
</ul> ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="<pre>[%a]<br />%m</pre>",pathFormat=" %p"}
</td>
</tr>
<tr>
<td> <hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td></td>
</tr>
<tr>
<td><b><font color="#0B610B">构建情况总览:</font></b>${TEST_COUNTS,var="fail"}<br/>
<hr size="2" width="100%" align="center" /></td>
</tr>
<tr>
<td><textarea cols="80" rows="30" readonly="readonly"
style="font-family: Courier New">${BUILD_LOG,maxLines=23}</textarea>
</td>
</tr>
</table>
</body>
</html>
  1. 在Jenkinsfile中添加发送email功能,post即指构建后操作
1
2
3
4
5
6
7
8
9
post {
always {
emailext(
subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} - ${BUILD_STATUS}!',
body: '${FILE,path="email.html"}',
to: 'chenqiming13@qq.com'
)
}
}
  1. 构建测试是否收到邮件

五、Jenkins+SonarQube代码审查

sonarqube是一个用于管理代码质量的开放平台,可以快速定位代码中潜在的错误。

环境要求:

  1. JDK11
  2. PostgreSQL

5.1 安装PostgreSQL

  1. 新版本的sonarqube不再支持MySQL,所以在Jenkins服务器上安装PostgreSQL并创建一个sonar数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#安装postgresql
yum -y install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
yum -y install postgresql96-server postgresql96-contrib

#初始化postgresql
postgresql-9.6-setup initdb
systemctl enable postgresql-9.6.service
systemctl start postgresql-9.6.service

#初始化之后会自动创建postgres用户,切换用户
su - postgres

#psql进入命令行模式配,\q退出
psql
alter user postgres with password 'toortoor';
create database sonar;
create user sonar;
alter user sonar with password 'toortoor';
alter role sonar createdb;
alter role sonar superuser;
alter role sonar createrole;
alter database sonar owner to sonar;
\q

  1. 开启远程访问和远程连接
1
2
3
4
5
vim /var/lib/pgsql/9.6/data/postgresql.conf
listen_addresses = '*'
vim /var/lib/pgsql/9.6/data/pg_hba.conf
host all all 127.0.0.1/32 trust
host all all 192.168.88.1/32 trust
  1. 重启服务

5.2 安装SonarQube

  1. 安装sonarqube
1
2
3
4
5
6
wget https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-8.7.1.42226.zip
unzip sonarqube-8.7.1.42226.zip
mv sonarqube-8.7.1.42226 sonarqube
useradd sonar
chown -R sonar:sonar sonarqube/*
mv sonarqube /opt/sonarqube
  1. 修改sonar配置文件
1
2
3
4
5
6
vim sonarqube/conf/sonar.properties
sonar.jdbc.username=sonar
sonar.jdbc.password=toortoor
sonar.jdbc.url=jdbc:postgresql://localhost/sonarqube?currentSchema=my_schema
sonar.jdbc.url=jdbc:postgresql://localhost/sonar?currentSchema=public
sonar.web.port=9000
  1. 启动sonarqube
1
2
3
#只能用sonar用户启动
su sonar
./opt/sonarqube/bin/linux-x86-64/sonar.sh start
  1. 遇到的问题
1
2
3
4
5
6
#sonar用户线程数不够,*代表所有用户
cat /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
* soft noproc 65535
* hard noproc 65535
  1. deploy.yaml
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
apiVersion: apps/v1
kind: Deployment
metadata:
name: sonarqube
labels:
app: sonarqube
spec:
replicas: 1
selector:
matchLabels:
app: sonarqube
template:
metadata:
name: sonarqube
labels:
app: sonarqube
spec:
initContainers:
- name: init-sysctl
image: busybox
imagePullPolicy: IfNotPresent
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
containers:
- name: sonarqube
image: sonarqube:lts-community
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9000
env:
# 此处用到了集群现有的pg
# 数据库用户sonarqube
# 数据库名sonarqube
- name: SONARQUBE_JDBC_USERNAME
value: "sonarqube"
- name: SONARQUBE_JDBC_PASSWORD
value: "dangerous"
- name: SONARQUBE_JDBC_URL
value: "jdbc:postgresql://dcs-installer-gitlab-postgresql.dcs-system:5432/sonarqube"
livenessProbe:
httpGet:
path: /sessions/new
port: 9000
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /sessions/new
port: 9000
initialDelaySeconds: 60
periodSeconds: 30
failureThreshold: 6
resources:
limits:
cpu: 2000m
memory: 2048Mi
requests:
cpu: 1000m
memory: 1024Mi

---
apiVersion: v1
kind: Service
metadata:
name: sonarqube
spec:
type: NodePort
selector:
app: sonarqube
ports:
- port: 9000
targetPort: 9000
protocol: TCP

5.3 Jenkins整合SonarQube

  1. 安装sonarqube scanner插件
  2. 在Jenkins安装sonarqube scanner

  1. 在sonarqube中生成密钥

  1. 在系统配置中配置sonarqube server,利用刚刚生成的密钥

5.4 实现代码审查

非流水线项目添加代码审查

  1. 在项目中添加构建步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#Must be unique in a given SonarQube instance
# SonarQube创建项目时的key
sonar.projectKey=freestyle_project

#This is the name and version displayed in the SonarQube UI.
# 项目名称
sonar.projectName=freestyle_project
# 项目版本
sonar.projectVersion=1.0

#Path is relative to the sonar-project.properties file.
#This property is optional if sonar.modules is set.
sonar.sources=.
# 忽略扫描的目录
sonar.exclusions=**/test/**,**/target/**

sonar.java.source=11
sonar.java.target=11

#Encoding of the source code.Default is default system encoding.
sonar.sourceEncoding=UTF-8
  1. 构建项目
  2. 回到sonarqube就可以看到提交的代码审查了

流水线项目添加代码审查

  1. 在项目中创建sonar-project.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#Must be unique in a given SonarQube instance
sonar.projectKey=pipeline_project

#This is the name and version displayed in the SonarQube UI.
sonar.projectName=pipeline_project
sonar.projectVersion=1.0

#Path is relative to the sonar-project.properties file.
#This property is optional if sonar.modules is set.
sonar.sources=.
sonar.exclusions=**/test/**,**/target/**

sonar.java.source=11
sonar.java.target=11

#Encoding of the source code.Default is default system encoding.
sonar.sourceEncoding=UTF-8
  1. 修改Jenkinsfile文件
1
2
3
4
5
6
7
8
9
10
11
12
stage('code checking'){
steps {
script {
//引入scanner工具
scannerHome = tool 'sonar-scanner'
}
//引入sonarqube服务器环境
withSonarQubeEnv('sonarqube') {
sh "${scannerHome}/bin/sonar-scanner"
}
}
}
  1. 将Jenkinsfile和sonar-project.properties推送到gitlab
  2. 构建项目后在sonarqube就可以看到代码审查了

六、Jenkins+Docker+SpringCloud微服务持续集成

大致流程:

  1. 开发人员push代码到gitlab。
  2. Jenkins服务器进行编译打包并构建镜像推送到harbor镜像仓库(服务器的JAVA环境要与项目对应)。
  3. Jenkins服务器触发远程命令使生产服务器(tomcat)从私有仓库拉取镜像。
  4. 生产服务器(tomcat)生成容器,项目上线。
  5. 用户访问。

6.1 Harbor部署

  1. 下载
1
curl -O https://github.com/goharbor/harbor/releases/download/v2.3.4/harbor-offline-installer-v2.3.4.tgz
  1. 修改 harbor.yml
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
cp harbor.yml.tmpl harbor.yml
egrep -v '^#|^$|*#' harbor.yml
hostname: 192.168.88.30
http:
port: 81
harbor_admin_password: toortoor
database:
password: toortoor
max_idle_conns: 100
max_open_conns: 900
data_volume: /root/cicd/harbor/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: /root/cicd/harbor/var
_version: 2.3.0
proxy:
http_proxy:
https_proxy:
no_proxy:
components:
- core
- jobservice
- trivy
  1. docker-compose.yaml 文件中指定 harbor 访问端口为 80,避免与 gitlab 冲突, 修改为 81

  2. 通过脚本部署

1
2
./prepare
./install.sh
  1. 创建私有仓库

  1. 修改 daemon.json,让 docker 信任此仓库
1
2
3
4
{
"registry-mirrors": ["https://5v5rh037.mirror.aliyuncs.com"],
"insecure-registries": ["192.168.88.30:81"]
}
  1. 登录远程镜像仓库
1
docker login -u admin -p toortoor 192.168.88.30:81

6.2 提交代码到Gitlab

6.3 Jenkins创建流水线工程

  1. 创建工程



  1. 在项目根目录下创建 Jenkinsfile,通过语法生成器进行编写

  1. Jenkinsfile 拉取代码部分

  1. 在流水线工程中创建参数,如果有多个服务就可以根据参数选择进行构建

  1. Jenkinsfile 添加编译打包微服务工程部分,以及通过 Dockerfile 构建镜像部分
  • 在 pom.xml 中添加 Dockerfile 依赖
1
2
3
4
5
6
7
8
9
10
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.10</version>
<configuration>
<repository>${project.artifactId}</repository>
<buildArgs>
<JAR_NAME>target/${project.build.finalname}.jar</JAR_NAME>
</buildArgs>
</plugin>
  • 在项目根目录添加 Dockerfile
1
2
3
4
5
6
FROM openjdk:11
ARG JAR_NAME
WORKDIR /usr/src/myapp
EXPOSE 8080
COPY ./${JAR_NAME} /usr/src/myapp/
ENTRYPOINT ["java", "-jar", "/usr/src/myapp/${JAR_NAME}"]
  1. Jenkinsfile 添加上传镜像至 Harbor 部分
  • 在 Jenkins 中添加 Harbor 账户凭证,以便于上传代码,这里用到的是 Harbor 的用户名和密码

  • 保留好凭证ID

  • 在语法生成器中生成新语法用于登录 Harbor 仓库

  1. Jenkins 安装 Publish Over SSH 插件,能够对远程主机发送 shell 命令,安装完后先给远程主机发送公钥
1
ssh-copy-id root@192.168.88.30
  • 发送完后在系统配置进行配置


  • 接着生成流水线语法,去远程执行脚本文件 test.sh

test.sh

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
#!/bin/bash

harbor_url=$1
project_name=$2
image_name=$3
service_port=$4

# 删除none容器
echo "docker rmi none..."
docker images | grep none | awk '{ print "docker rmi "$3 }' | sh &>/dev/null

# 检查是否有已运行容器
num=`docker ps | grep $project_name | wc -l`
if [ $num -gt 0 ]
then
docker stop $project_name && docker rm $project_name &>/dev/null
if [ `echo $?` -eq 0 ]
then
echo "docker stop $project_name && docker rm $project_name [OK]"
else
echo "ERROR: docker stop $project_name && docker rm $project_name"
fi
fi

# 检查是否有重命名镜像
temp=`docker images | grep $image_name | awk '{ print $1":"$2 }' | wc -l`
if [ $temp -eq 1 ]
then
docker rmi $image_name &>/dev/null
if [ `echo $?` -eq 0 ]
then
echo "docker rmi $image_name [OK]"
else
echo "ERROR: docker rmi $image_name"
fi
fi

# 登录镜像仓库
echo "docker login repository..."
docker login -u admin -p toortoor $harbor_url &>/dev/null
if [ `echo $?` -ne 0 ]
then
echo "ERROR: failed to login repository"
else
echo "docker login repository [OK]"
fi

# 拉取镜像
echo "docker pull image..."
docker pull $image_name &>/dev/null
if [ `echo $?` -ne 0 ]
then
echo "ERROR: failed to pull image"
else
echo "docker pull image [OK]"
fi

# 运行容器
echo "docker run container..."
docker run -d --name $project_name -p $service_port:8080 $image_name &>/dev/null
if [ `echo $?` -ne 0 ]
then
echo "ERROR: failed to run container"
else
echo "docker run container [OK]"
fi
  1. 最终的 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
// git凭证id
def git_auth = "bad37d0d-7260-43a2-a350-9abf6e99753a"
// git凭证url
def git_url = "http://192.168.88.30/test_group/test_project.git"
// Harbor地址
def harbor_url = "192.168.88.30:81"
// 镜像仓库名称
def harbor_repository = "test"
// Harbor用户凭证ID
def harbor_auth = "c147d241-a254-48d6-be1c-0d5f0964ce9a"
// 镜像版本号
def image_version = "1.0"
// 拉取的微服务名称
def project_name = "test"
// 微服务所需端口号
def service_port = "8081"


node {
// 拉取代码
stage('Pull Code') {
git branch: "${branch}", credentialsId: "${git_auth}", url: "${git_url}"
}

// 编译,打包微服务工程,构建镜像
stage('Mvn clean package and Docker build') {
sh "mvn clean package dockerfile:build"
}

// 上传镜像至Harbor
stage('Push image to Harbor') {
// 镜像命名,打标签
def image_name = "${project_name}:${image_version}"
sh "docker tag ${project_name}:latest ${harbor_url}/${harbor_repository}/${image_name} && docker rmi ${project_name}:latest"
// 登录Harbor仓库
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'harbor_password', usernameVariable: 'harbor_user')]) {
sh "docker login -u ${harbor_user} -p ${harbor_password} ${harbor_url}"
}
// 镜像上传
sh "docker push ${harbor_url}/${harbor_repository}/${project_name}:${image_version}"
}

// 远程连接服务器拉取镜像运行
stage('Pull image and Running container') {
// 获取镜像命名
def image_name = "${project_name}:${image_version}"
sshPublisher(publishers: [sshPublisherDesc(configName: '192.168.88.30', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: './root/test.sh $harbor_url $project_name $harbor_url/$harbor_repository/image_name $service_port', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}

6.4 微服务架构CICD优化

从以上配置中可以看到,每次构建都只能选择一个服务,且部署的服务器也只有一台,是不符合生产环境的,主要需求有以下三个:

  • 多个微服务同时构建流水线工程
  • 批量处理镜像
  • 将微服务部署服务器集群
  1. 安装 Extended Choice Parameter 插件,实现多个微服务同时构建

  1. 在创建流水线时设置选项



  1. 将公钥下发,并在系统设置的 Publish over SSH 中添加多台主机
  2. 在流水线工程中添加多一个多选项配置,用于选择要部署的服务器

  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
// git凭证id
def git_auth = "bad37d0d-7260-43a2-a350-9abf6e99753a"
// git凭证url
def git_url = "http://192.168.88.30/test_group/test_project.git"
// Harbor地址
def harbor_url = "192.168.88.30:81"
// 镜像仓库名称
def harbor_repository = "test"
// Harbor用户凭证ID
def harbor_auth = "c147d241-a254-48d6-be1c-0d5f0964ce9a"
// 镜像版本号
def image_version = "1.0"

node {
// 获取服务名
def selectedProjectNames = "${project_name}".split(",")
// 获取集群地址
def selectedServices = "${publish_server}".split(",")

// 拉取代码
stage('Pull Code') {
git branch: "${branch}", credentialsId: "${git_auth}", url: "${git_url}"
}

// 编译,打包微服务工程,构建镜像
stage('Mvn clean package and Docker build') {
sh "mvn clean package dockerfile:build"
}

// 上传镜像至Harbor
stage('Push image to Harbor') {
for(int i=0; i<selectedProjectNames.length; i++) {
// 获取每个选项
def project_info = selectedProjectNames[i]
// 获取选项中的微服务名称
def current_project_name = "${project_info}".split("@")[0]
// 镜像命名,打标签
def image_name = "${current_project_name}:${image_version}"
sh "docker tag ${current_project_name}:latest ${harbor_url}/${harbor_repository}/${image_name} && docker rmi ${current_project_name}:latest"
// 登录Harbor仓库
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'harbor_password', usernameVariable: 'harbor_user')]) {
sh "docker login -u ${harbor_user} -p ${harbor_password} ${harbor_url}"
}
// 镜像上传
sh "docker push ${harbor_url}/${harbor_repository}/${current_project_name}:${image_version}"
}
}

// 远程连接服务器拉取镜像运行
stage('Pull image and Running container') {
for(int i=0; i<selectedProjectNames.length; i++) {
// 获取每个选项
def project_info = selectedProjectNames[i]
// 获取选项中的微服务名称
def current_project_name = "${project_info}".split("@")[0]
// 获取选项中的微服务所需端口
def current_project_port = "${project_info}".split("@")[1]
// 需要拉取的镜像
def image_name = "${current_project_name}:${image_version}"
// 遍历所有服务器,分别部署
for(int j=0; j<selectedServices.length; j++) {
// 获取当前服务器
def current_server = selectedServices[j]
// 加上参数配置,根据配置文件的不同选择不同的服务器部署
if(current_server == "192.168.88.31") {
activeProfile = activeProfile+"serviceName-server1"
}else if(current_server == "192.168.88.32") {
activeProfile = activeProfile+"serviceName-server2"
}
// 远程执行脚本
sshPublisher(publishers: [sshPublisherDesc(configName: "${current_server}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: './root/test.sh $harbor_url $current_project_name $harbor_url/$harbor_repository/$image_name $current_project_port $activeProfile', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}
}
}
  1. 修改 test.sh
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
#!/bin/bash

harbor_url=$1
project_name=$2
image_name=$3
service_port=$4
profile=$6

# 删除none容器
echo "docker rmi none..."
docker images | grep none | awk '{ print "docker rmi "$3 }' | sh &>/dev/null

# 检查是否有已运行容器
num=`docker ps | grep $project_name | wc -l`
if [ $num -gt 0 ]
then
docker stop $project_name && docker rm $project_name &>/dev/null
if [ `echo $?` -eq 0 ]
then
echo "docker stop $project_name && docker rm $project_name [OK]"
else
echo "ERROR: docker stop $project_name && docker rm $project_name"
fi
fi

# 检查是否有重命名镜像
temp=`docker images | grep $image_name | awk '{ print $1":"$2 }' | wc -l`
if [ $temp -eq 1 ]
then
docker rmi $image_name &>/dev/null
if [ `echo $?` -eq 0 ]
then
echo "docker rmi $image_name [OK]"
else
echo "ERROR: docker rmi $image_name"
fi
fi

# 登录镜像仓库
echo "docker login repository..."
docker login -u admin -p toortoor $harbor_url &>/dev/null
if [ `echo $?` -ne 0 ]
then
echo "ERROR: failed to login repository"
else
echo "docker login repository [OK]"
fi

# 拉取镜像
echo "docker pull image..."
docker pull $image_name &>/dev/null
if [ `echo $?` -ne 0 ]
then
echo "ERROR: failed to pull image"
else
echo "docker pull image [OK]"
fi

# 运行容器
echo "docker run container..."
docker run -d --name $project_name -p $service_port:8080 $image_name $profile &>/dev/null
if [ `echo $?` -ne 0 ]
then
echo "ERROR: failed to run container"
else
echo "docker run container [OK]"
fi

七、基于K8S的CICD

基于 Kubernetes 平台来实现 CICD 功能。

7.1 Jenkins对接K8S

  • 首先通过 bitnami helm 部署 Jenkins(Master),需开放 jnlp 50000 端口
1
2
3
4
5
6
7
# 添加repo源
helm repo add bitnami https://charts.bitnami.com/bitnami
# 下载chart到本地
helm pull bitnami/jenkins
# 根据需求修改values.yaml
# 部署
helm install jenkins jeknins/

  • RBAC 授权,SA 名为 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: nextcloud

---

kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: jenkins
namespace: nextcloud
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create","delete","get","list","patch","update","watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create","delete","get","list","patch","update","watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get","list","watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
- apiGroups: ["extensions", "apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: jenkins
namespace: nextcloud
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
namespace: nextcloud
  • 获取刚刚创建的 ca.crt 信息
1
kubectl get secret jenkins-token-xxx -oyaml | awk '/ca.crt/{ print $2 }' | base64 -d
  • 在 Jenkins 创建凭据

  • 添加云,这里用到刚刚创建的凭据



设置 Pod Template,即 Jenkins Slave,将包含 jnlp 一个容器,来实现编译打包等功能。



这里需挂载宿主机的 docker.sock、docker 和 kubectl 的二进制文件。


然后使用到刚刚创建的 SA。


测试链接。

7.2 构建前准备

需要在项目的根目录下准备好 Dockerfile、Jenkinsfile、deploy.yaml。

  • 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
// gitlab及harbur凭据认证等信息
def git_auth = "764f29f3-8955-4551-a23a-659aa4c8deaa"
def git_url = "http://192.168.159.12:30098/root/nextcloud.git"
def harbor_url = "192.168.159.101"
def harbor_repository = "nextcloud"
def harbor_auth = "7ebc36f8-73a3-417f-864f-24856490b1a2"
def project_name = "nextcloud"

// jenkins-slave为pod template中的name
node('jenkins-slave') {

// jnlp为pod template中的容器名称
container('jnlp') {

// pipeline步骤
stage('Git Clone') {
git branch: "${branch}", credentialsId: "${git_auth}", url: "${git_url}"
}

stage('Docker Build') {
sh "docker build -t ${harbor_url}/${harbor_repository}/${project_name}:${branch}-${version} ."
}

stage('Docker Push') {
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'harbor_password', usernameVariable: 'harbor_user')]) {
sh "docker login -u ${harbor_user} -p ${harbor_password} ${harbor_url}"
}
sh "docker push ${harbor_url}/${harbor_repository}/${project_name}:${branch}-${version}"
sh "docker rmi ${harbor_url}/${harbor_repository}/${project_name}:${branch}-${version}"
}
}

// 在deploy.yaml中将镜像tag设为了build-tag,方便通过sed修改
stage('Sed YAML') {
sh "sed -i 's#build-tag#${branch}-${version}#g' deploy.yaml"
}

stage('Deploy to K8s') {
sh "kubectl apply -f deploy.yaml"
}

}

7.3 创建流水线项目

这里添加了两个字符参数,用于构建前选择分支和 tag 的标签定义。



构建测试,会发现集群中创建了一个 jenkins-slave-xxx 的 pod,pod 中包含 jnlp 一个容器,通过观察日志可发现与 Jenkins Master 进行了连接。


Jenkins 中查看流水线进度。


流水线结束后,通过 kubectl 可看到项目已部署,且使用的镜像是刚刚构建好的。

Author

Warner Chen

Posted on

2024-02-18

Updated on

2025-05-29

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.