Jenkins在日常工作中占据了一个非常重要的角色,帮助我们节省了大量用于构建的时间。有些公司有运维大哥对Jenkins进行维护,如果没有那只能自己动手了。俗话说的好自己动手丰衣足食,所以本文就从0开始搭建属于自己的Jenkins持续平台。主要包含, 普通项目构建 、 流水线构建 、 多分支流水线构建 并将构建结果辅以钉钉通知。
Docker要求CentOS系统的内核版本高于3.10.通过 uname -r
命令查看你当前的内核版本。
[root@CentOS ~]# uname -r 3.10.0-1127.8.2.el7.x86_64 复制代码
备份旧源
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 复制代码
下载最新的源
wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo 复制代码
生成缓存
yum makecache 复制代码
更新
yum update 复制代码
官方安装文档
yum install -y yum-utils 复制代码
添加docker源
yum-config-manager / --add-repo / https://download.docker.com/linux/centos/docker-ce.repo 复制代码
安装docker
yum install docker-ce 复制代码
systemctl start docker 复制代码
vim /etc/docker/daemon.json 复制代码
加入阿里云源地址
{ "registry-mirrors":["https://6kx4zyno.mirror.aliyuncs.com"] } 复制代码
重新读取配置
systemctl daemon-reload 复制代码
重启docker
systemctl restart docker 复制代码
下载jenkins镜像
docker pull jenkins 复制代码
设置端口为9090并映射 jenkins_home
到宿主机 /home/jenkins_home
。
docker run -d --name jenkins -p 9090:8080 -v /home/jenkins_home:/var/jenkins_home jenkins 复制代码
可以通过 docker ps
查看运行的容器。
[root@CentOS home]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ec6a4da6b83f jenkins "/bin/tini -- /usr/l…" About a minute ago Up About a minute 50000/tcp, 0.0.0.0:9090->8080/tcp jenkins [root@CentOS home]# 复制代码
在运行启动jenkins的命令时,可能会出现jenkins无法启动情况。
[root@CentOS home]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b571f16dafbf jenkins "/bin/tini -- /usr/l…" 8 minutes ago Exited (1) 8 minutes ago jenkins [root@CentOS home]# 复制代码
可以通过 docker logs 镜像名称
查看启动日志。
[root@CentOS home]# docker logs jenkins touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions? [root@CentOS home]# 复制代码
查看输出的日志,如果出现 Permission denied
类似的错误。需要删除旧容器重新运行。
docker rm jenkins 复制代码
运行命令加入了 -u 0
重新运行。
docker run -d --name jenkins -p 9090:8080 -v /home/jenkins_home:/var/jenkins_home -u 0 jenkins 复制代码
参考 https://blog.csdn.net/minicto/article/details/73539986
启动成功后输入 http://服务器:9090/
首次启动jenkins需要输入密码,需要进入容器内获取密码。密码位于 /var/jenkins_home/secrets/initialAdminPassword
。
进入容器
docker exec -it jenkins /bin/bash 复制代码
获取密码
cat /var/jenkins_home/secrets/initialAdminPassword 复制代码
[root@CentOS jenkins_home]# docker exec -it jenkins /bin/bash root@ec6a4da6b83f:/# cat /var/jenkins_home/secrets/initialAdminPassword 68eed23ad39541949972468e4f2ce1fd root@ec6a4da6b83f:/# 复制代码
由于我们将 /var/jenkins_home
-- 挂载到--> /home/jenkins_home
所以也可以直接 cat /home/jenkins_home/secrets/initialAdminPassword
获取密码。
输入密码以后,安装需要的插件,在安装途中由于网络原因会出现有些插件安装失败,这个可以不用理会。
设置jenkins的默认登录账号和密码
进入jenkins的主页面右上角可能会出现一些报错信息,主要是提示jenkins 需要的某些插件没有安装,或者说jenkins版本太低了,插件无法使用这个时候我们需要先升级jenkins做一个升级。
Jenkins提供了自动升级的方式
可以去Jenkins的官网下载好最新jar包上传到服务器,也可以使用 wget
命令。
wget http://jenkins新版本的下载地址 #目前最新2.239 wget http://updates.jenkins-ci.org/download/war/2.239/jenkins.war 复制代码
Jenkins的更新主要是替换jenkins镜像里面的war包 ,我们可以把下载好的war包使用 docker cp
直接进行复制命令如下:
docker cp jenkins.war jenkins:/usr/share/jenkins 复制代码
重新启动Jenkins即可完成升级。
docker restart jenkins 复制代码
更插件源
https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json 复制代码
主要配置 jdk、maven、git等常用环境。需要注意配置的别名,后续构建将会使用到。
因为jenkins镜像自带jdk所以无需安装直接使用即可,进入Jenkins容器,使用 java -verbose
查看java安装路径。
docker exec -it jenkins /bin/bash 复制代码
java -verbose 复制代码
进入容器内使用 whereis git
即可查询到git安装路径。
root@6a9fbb129cbe:~# whereis git git: /usr/bin/git /usr/share/man/man1/git.1.gz root@6a9fbb129cbe:~# 复制代码
maven直接使用自动安装即可。
点击新增即可添加服务器,主要配置:
Name
名称 - 构建的时候将会用到 Hostname
服务器地址 Username
用户名 Remote Directory
远程目录 - 上传文件的目录 默认配置根目录即可 /
。 点击高级进行其他参数配置
Use password authentication, or use a different key
复选框即可,如下图所示。 除了配置密码还可以配置端口 Port
,跳板机 Jump Host
的参数,可以根据实际情况配置。默认可以使用密码。
配置完成以后点击 Test Configuration
按钮,如果配置正常会出现 Success
反之出现错误信息,可以根据错误信息,调整配置参数。
钉钉主要用于构建通知,在配置前需要在钉钉群内,添加自定义机器人。
以 https://gitee.com/huangxunhui/jenkins_demo.git
为例。
如果项目是开源,则可以跳过这一步。反之需要设置凭证,要不然将无法拉取代码进行构建。
可以根据实际情况选择,案例采用轮询的方式进行构建。
Source files
jar包的路径。支持通配符匹配. Remove prefix
移除前缀,一般jar包的路径都存在于 **/target
下,如果不移除,会在目标服务器上建立相应的目录结构。 Remote directory
远程目录。 注意的点, 在之前配置服务器时也配置了 Remote directory
,这时候部署的实际目录是,服务器设置的远程目录+现在配置的远程目录。
Exec command
执行脚本,主要用于将 jar
发送到目标服务器后,执行相应的启动脚本。 配置完成点击保存即可。
点击开始构建
发送钉钉通知
流水线构建,将上述构建步骤代码化,方便调整。
由于配置步骤类似,前面简单的步骤可以参照, 自由风格的软件项目
。这里主要讲流水线如何编写。
注意右下角的流水线语法,后续会用上。
我们可以点击右上角的下拉按钮,生成一个简单的流水线。比如说hello world。
pipeline { // 表示所有机器都能运行 agent any stages { stage('Hello') { steps { echo 'Hello World' } } } } 复制代码
通过上面的 pipeline
可以知道,有一个 Hello
的步骤,这个步骤执行的是,输出 hello world
。依葫芦画瓢,一次完整的构建我们可以总结出如下几个步骤:拉取代码(checkout) -> 打包(build) -> 部署(deploy)。
pipeline { agent any stages { stage('checkout') { steps { } } stage('build') { steps { } } stage('deploy') { steps { } } } } 复制代码
步骤梳理好了,这个时候就可以完善对应的步骤了,这就需要用到提到的,流水线语法。
将生成好的流水线脚本复制到对应的步骤即可。
注意:如果使用到maven需要将maven引入, tools
相应的内容就是配置 maven
时配置的别名。
pipeline { agent any // 工具 tools { maven 'maven' jdk 'jdk' } stages { stage('checkout') { steps { checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[url: 'https://gitee.com/huangxunhui/jenkins_demo.git']]]) } } stage('build') { steps { sh 'mvn clean package -Dmaven.test.skip=true' } } stage('deploy') { steps { sshPublisher(publishers: [sshPublisherDesc(configName: 'dev', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '''cd /home/project nohup java -jar jenkins_demo.jar > nohup.out 2>&1 &''', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/home/project', remoteDirectorySDF: false, removePrefix: '/target', sourceFiles: '**/target/jenkins_demo.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) } } } } 复制代码
配置完成点击应用即可。
构建测试
上面演示的是将流水线配置在jenkins内,其实我们还可以从 SCM
中获取,比如 git
。
我们可以建立一个仓库专门维护不同项目的构建脚本 Jenkinsfile
,也可以在每个项目下,建立对应的 Jenkinsfile
.
Jenkinsfile
需要和配置的一致。比如说上面的配置,是扫描项目根目录下名字为 Jenkinsfile
的文件。 所以我们可以在 jenkins_demo
仓库内添加 Jenkinsfile
文件。
配置点击完成,即可。
在日常开发中,通常是基于 git-flow
进行开发的,前面两种都是基于单分支构建,如果每个分支都去配置,那将耗费大量时间。所以 多分支流水线 就是用来解决这个问题的。
完成上述配置,点击应用即可。
核心思想是,根据不同的分支使用不同的打包命令,发送到不同的服务器进行运行。
pipeline { // 指定集群 any 表示所有 agent any // 工具 tools { maven 'maven' jdk 'jdk' } // 定义常量 environment { // 钉钉机器人编号 rebootId = 'a3c07482-d031-47a6-8542-05ac56c5f17a' // 开始logo imageOfStart = 'https://www.easyicon.net/api/resizeApi.php?id=1229977&size=128' // 成功logo imageOfSuccess = 'https://www.easyicon.net/api/resizeApi.php?id=1194837&size=128' // 失败logo imageOfFailure = 'https://www.easyicon.net/api/resizeApi.php?id=1201052&size=128' // 不稳定logo imageOfUnstable = 'https://www.easyicon.net/api/resizeApi.php?id=1219854&size=128' // 终止logo imageOfAborted = 'https://www.easyicon.net/api/resizeApi.php?id=1183198&size=128' // 认证Id credentialsId = '98e9c197-f0ae-44c3-8f67-4ca0339028a8' // 仓库地址 repositoryUrl = 'https://gitee.com/huangxunhui/jenkins_demo.git' // 打包命令 - 项目需要配置maven多环境 mavenProd = 'mvn clean package -P prod -Dmaven.test.skip=true' mavenTest = 'mvn clean package -P test -Dmaven.test.skip=true' mavenDev = 'mvn clean package -P dev -Dmaven.test.skip=true' // 服务器名称 - 案例测试-全部部署到dev环境 devServer = 'dev' testServer = 'dev' prodServer = 'dev' // sshPublisher 配置 removePrefix = '/target' remoteDirectory = '/home/project/jenkins_demo' sourceFiles = '**/target/jenkins_demo.jar' execCommandProd = 'cd /home/project && ./manage.sh jenkins_demo/ restart' execCommandTest = 'cd /home/project && ./manage.sh jenkins_demo/ restart' execCommandDev = 'cd /home/project && ./manage.sh jenkins_demo/ restart' } stages { stage('开始构建通知'){ steps { dingtalk ( robot: "${rebootId}", type: 'LINK', title: "${env.JOB_NAME}", text: [ "开始构建-编号为#${BUILD_NUMBER}" ], messageUrl: "${env.BUILD_URL}", picUrl: "${imageOfStart}" ) } } stage('拉取代码'){ steps { echo "拉取 ${BRANCH_NAME} 分支的代码。" checkout([$class: 'GitSCM', branches: [[name: "*/${BRANCH_NAME}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: "${credentialsId}", url: "${repositoryUrl}"]]]) } } stage('进行打包'){ steps { script { if (env.BRANCH_NAME == 'master') { sh "${mavenProd}" } else if (env.BRANCH_NAME == 'test') { sh "${mavenTest}" } else if (env.BRANCH_NAME == 'dev') { sh "${mavenDev}" } else { sh "${mavenDev}" } } } } stage('项目部署'){ steps { script { if (env.BRANCH_NAME == 'master') { // 部署生产环境 sshPublisher(publishers: [sshPublisherDesc(configName: "${prodServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandProd}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) } else if (env.BRANCH_NAME == 'test') { // 部署测试环境 sshPublisher(publishers: [sshPublisherDesc(configName: "${testServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandTest}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) } else if (env.BRANCH_NAME == 'dev') { // 部署开发环境 sshPublisher(publishers: [sshPublisherDesc(configName: "${devServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandDev}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) } else { sshPublisher(publishers: [sshPublisherDesc(configName: "${devServer}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "${execCommandTest}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: "${remoteDirectory}", remoteDirectorySDF: false, removePrefix: "${removePrefix}", sourceFiles: "${sourceFiles}")], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) } } } } } // 流水线结束通知 post { // 成功通知 success { dingtalk ( robot: "${rebootId}", type: 'LINK', title: "${env.JOB_NAME}", text: [ "构建成功-编号为#${BUILD_NUMBER}" ], messageUrl: "${env.BUILD_URL}", picUrl: "${imageOfSuccess}" ) } // 失败通知 failure { dingtalk ( robot: "${rebootId}", type: 'LINK', title: "${env.JOB_NAME}", text: [ "构建失败-编号为#${BUILD_NUMBER}" ], messageUrl: "${env.BUILD_URL}", picUrl: "${imageOfFailure}" ) } // 构建不稳定通知 unstable { dingtalk ( robot: "${rebootId}", type: 'LINK', title: "${env.JOB_NAME}", text: [ "构建不稳定-编号为#${BUILD_NUMBER}" ], messageUrl: "${env.BUILD_URL}", picUrl: "${imageOfUnstable}" ) } // 构建终止通知 aborted { dingtalk ( robot: "${rebootId}", type: 'LINK', title: "${env.JOB_NAME}", text: [ "构建终止-编号为#${BUILD_NUMBER}" ], messageUrl: "${env.BUILD_URL}", picUrl: "${imageOfAborted}" ) } } } 复制代码
使用到的启动脚本 manage.sh
。
钉钉机器人插件使用文档
如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。