1 什么是DevOps
软件开发最高效的组织形式是“One Man Work”,只有一个人干活,写个小项目,从需求到开发,从测试到部署全部独立完成,非常高效。但随着业务的增长,项目开始逐渐变得庞大,变成团队,出现了分工,出现了产品经理、项目经理、开发、数据、测试、运维等等角色。这些角色间存在天然的工作目标上的矛盾。
举个例子,对于运维来说,稳定压倒一切,新 Feature 越少越好。而对于研发来说,却希望能开发更多的功能。这种矛盾会导致大量的资源和时间的浪费。就像两匹马拉一辆车,如果马头向着的方向不一致,肯定是没法全速前进的。
DevOps 的理念就是希望能打破这种屏障,让研发(Development)和运维(Operations)一体化,让团队从业务需求出发,向着同一个目标前进。
字面意思上说 DevOps 是指“开发运维一体化”,即通过工具辅助开发完成运维的部分工作,减少成本。但深入理解了 DevOps 之后,你会发现 DevOps 其实是一种软件研发管理的思想,方法论,他追求的是一种没有隔阂的理想的研发协作的状态,可能涉及到的角色有开发、测试、产品、项目管理、运维等等。所以我们认为,为了帮助研发团队在保持质量的前提下提高交付效率的方法和方法论都隶属于 DevOps 的范畴。
比如 Google 提出的 5 个 DevOps 原则,这套原则中必须依赖于工具辅助的部分只有后两点,更多的则是对于开发组织形式的内省:
(1) 精简组织架构;
(2) 愿意承担一部分试错带来的损失;
(3) 分阶段地一小步一小步地进行转型;
(4) 最大化地利用工具和自动化流程;
(5) 对所有的过程和结果进行记录和分析。
所以 DevOps 不是简单的开发软件化,而是企业的学习能力不断提升的结果,将企业改造成敏捷应对的学习型组织,运用新的工具,优化组织架构和流程,不断地进行自我革命和创新的方式。工具是辅助,而非基础。
2 CI/CD
如果把我们一个项目从开发到部署结束,我们可以抽出一个过程,编码、打包、集成、测试、交付、部署。
2.1 持续集成 (Continuous Integration)
持续集成(CI)可以帮助开发人员更加频繁地(有时甚至每天)将代码更改合并到共享分支或“主干”中。一旦开发人员对应用所做的更改被合并,系统就会通过自动构建应用并运行不同级别的自动化测试(通常是单元测试和集成测试)来验证这些更改,确保这些更改没有对应用造成破坏。这意味着测试内容涵盖了从类和函数到构成整个应用的不同模块。如果自动化测试发现新代码和现有代码之间存在冲突,CI 可以更加轻松地快速修复这些错误。
2.2 持续交付(Continuous Delivery)
完成 CI 中构建及单元测试和集成测试的自动化流程后,持续交付可自动将已验证的代码发布到存储库。为了实现高效的持续交付流程,务必要确保 CI 已内置于开发管道。持续交付的目标是拥有一个可随时部署到生产环境的代码库。
2.3 持续部署(Continuous Deployment)
对于一个成熟的 CI/CD 管道来说,最后的阶段是持续部署。作为持续交付——自动将生产就绪型构建版本发布到代码存储库——的延伸,持续部署可以自动将应用发布到生产环境。由于在生产之前的管道阶段没有手动门控,因此持续部署在很大程度上都得依赖精心设计的测试自动化。
3 SpringCloud持续集成解决方案
3.1 将代码上传到gitlab
3.2 Jenkins整合gitlab
3.2.1 新建pipeline项目
3.2.2 Webhook Trigger
(1) 配置webhook
(2) 配置 Generic Webhook Trigger
(3) 获取Jenkinsfile
3.2.3 Gitlab 设置 WebHook
http://192.168.38.45:8000/jenkins/generic-webhook-trigger/invoke?token=springcloud
3.2.4 生成拉取代码脚本
checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '0543fa76-913f-4600-9a3b-30ac70c0fc86', url: 'git@192.168.38.45:root/springcloud-demo.git']]])
3.2.5 编写Jenkinsfile
node {
stage('拉取代码') {
checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '0543fa76-913f-4600-9a3b-30ac70c0fc86', url: 'git@192.168.38.45:root/springcloud-demo.git']]])
}
}
3.2.6 集成测试
3.3 制作镜像,上传harbor
3.3.1 每个服务添加Dokcerfile
###############端口不同#######################
#FROM java:8
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
EXPOSE 9000
ENTRYPOINT ["java","-jar","/app.jar"]
#FROM java:8
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
EXPOSE 8001
ENTRYPOINT ["java","-jar","/app.jar"]
#FROM java:8
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
EXPOSE 8002
ENTRYPOINT ["java","-jar","/app.jar"]
3.3.2 Maven添加Dockerfile插件
<!-- docker的maven插件-->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<repository>${project.artifactId}</repository>
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
3.3.3 清除上次镜像脚本
#! /bin/sh
#参数
projectImage=$1
#判断镜像是否存在
for imageId in `docker images | grep -w $projectImage | awk '{print $3}'`
do
docker rmi -f ${imageId} &>/dev/null
done
echo "删除${projectImage}镜像成功"
3.3.4 Jenkins集成Harbor
(1) 创建Harbor凭证
(2)记录生成的ID
dd671782-a857-4884-8307-f1084aef867d
(3) 生成脚本
withCredentials([usernamePassword(credentialsId: 'dd671782-a857-4884-8307-f1084aef867d', passwordVariable: 'password', usernameVariable: 'username')]) {
// some block
}
3.3.5 修改Jenkinsfile
//=================定义公用信息===============
//harbor地址
def harborUrl = "192.168.38.100"
//harbor仓库名称
def harborRepository = "springcloud"
//构建版本tag
def tag = "latest"
//harbor凭证
def harborAuth = "dd671782-a857-4884-8307-f1084aef867d"
//定义函数,获取有效的服务
def getValidServerList(){
//需要构建的服务名
def buildServerNames = ['gateway-server','student-server']
//所有服务列表
def springCloudServers= [['serverName':'gateway-server','serverPort':9000],
['serverName':'student-server','serverPort':8001],
['serverName':'teacher-server','serverPort':8002]]
//返回结果集
def rs=[];
for(int i=0;i<buildServerNames.size();i++){
for(int j=0;j<springCloudServers.size();j++){
if("${buildServerNames[i]}" == "${springCloudServers[j].serverName}"){
rs.add(springCloudServers[j])
}
}
}
return rs;
}
node {
//有效的服务列表
def validServerList=getValidServerList()
stage('拉取代码') {
checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '0543fa76-913f-4600-9a3b-30ac70c0fc86', url: 'git@192.168.38.45:root/springcloud-demo.git']]])
}
stage('制作镜像,上传harbor'){
for(int i=0;i<validServerList.size();i++){
echo "制作镜像: ${validServerList[i].serverName}"
//服务名称
def serverName = "${validServerList[i].serverName}"
//每次构建前,清除上次构建的镜像
sh "/usr/local/shell/jenkins/clearImages.sh ${serverName}"
//打包,构建镜像
sh "mvn -f ${serverName} clean package dockerfile:build"
// 定义镜像名称
def imageName="${serverName}:${tag}"
// 打tag
sh "docker tag ${imageName} ${harborUrl}/${harborRepository}/${imageName}"
// 上传镜像
withCredentials([usernamePassword(credentialsId: "${harborAuth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harborUrl}"
//上传
sh "docker push ${harborUrl}/${harborRepository}/${imageName}"
}
}
}
}
3.3.6 集成测试
(1) 推develop分支代码到gitlab
(2) 触发流水线
(3) 查看镜像
(4) 查看harbor
3.4 SonarQube代码审查
3.4.1 添加sonar配置文件
(1) gateway-server
#SonarQube 实例名称,唯一
sonar.projectKey=gateway-server
#项目名称
sonar.projectName=gateway-server
#版本号
sonar.projectVersion=1.0
#指定扫描目录
sonar.sources=.
sonar.exclusions=**/test/**
#jdk版本
sonar.java.source=1.8
sonar.java.target=1.8
#扫描编写类的项目
sonar.java.binaries=target/classes
#编码格式
sonar.sourceEncoding=UTF-8
(2) student-server
#SonarQube 实例名称,唯一
sonar.projectKey=student-server
#项目名称
sonar.projectName=student-server
#版本号
sonar.projectVersion=1.0
#指定扫描目录
sonar.sources=.
sonar.exclusions=**/test/**
#jdk版本
sonar.java.source=1.8
sonar.java.target=1.8
#扫描编写类的项目
sonar.java.binaries=target/classes
#编码格式
sonar.sourceEncoding=UTF-8
(3) teacher-server
#SonarQube 实例名称,唯一
sonar.projectKey=teacher-server
#项目名称
sonar.projectName=teacher-server
#版本号
sonar.projectVersion=1.0
#指定扫描目录
sonar.sources=.
sonar.exclusions=**/test/**
#jdk版本
sonar.java.source=1.8
sonar.java.target=1.8
#扫描编写类的项目
sonar.java.binaries=target/classes
#编码格式
sonar.sourceEncoding=UTF-8
3.4.2 修改Jenkinsfile
//=================定义公用信息===============
//harbor地址
def harborUrl = "192.168.38.100"
//harbor仓库名称
def harborRepository = "springcloud"
//构建版本tag
def tag = "latest"
//harbor凭证
def harborAuth = "dd671782-a857-4884-8307-f1084aef867d"
//定义函数,获取有效的服务
def getValidServerList(){
//需要构建的服务名
def buildServerNames = ['gateway-server','student-server']
//所有服务列表
def springCloudServers= [['serverName':'gateway-server','serverPort':9000],
['serverName':'student-server','serverPort':8001],
['serverName':'teacher-server','serverPort':8002]]
//返回结果集
def rs=[];
for(int i=0;i<buildServerNames.size();i++){
for(int j=0;j<springCloudServers.size();j++){
if("${buildServerNames[i]}" == "${springCloudServers[j].serverName}"){
rs.add(springCloudServers[j])
}
}
}
return rs;
}
node {
//有效的服务列表
def validServerList=getValidServerList()
stage('拉取代码') {
checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '0543fa76-913f-4600-9a3b-30ac70c0fc86', url: 'git@192.168.38.45:root/springcloud-demo.git']]])
}
stage('制作镜像,上传harbor'){
for(int i=0;i<validServerList.size();i++){
echo "制作镜像: ${validServerList[i].serverName}"
//服务名称
def serverName = "${validServerList[i].serverName}"
//每次构建前,清除上次构建的镜像
sh "/usr/local/shell/jenkins/clearImages.sh ${serverName}"
//打包,构建镜像
sh "mvn -f ${serverName} clean package dockerfile:build"
// 定义镜像名称
def imageName="${serverName}:${tag}"
// 打tag
sh "docker tag ${imageName} ${harborUrl}/${harborRepository}/${imageName}"
// 上传镜像
withCredentials([usernamePassword(credentialsId: "${harborAuth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harborUrl}"
//上传
sh "docker push ${harborUrl}/${harborRepository}/${imageName}"
}
}
}
stage('代码审查'){
//定义当前Jenkins的SonarQubeScanner工具
def scannerHome = tool 'sonarqube-scanner'
for(int i=0;i<validServerList.size();i++){
//引用当前JenkinsSonarQube环境
withSonarQubeEnv('sonarqube') {
sh """cd ${validServerList[i].serverName}
${scannerHome}/bin/sonar-scanner"""
}
}
}
}
3.4.3 集成测试
(1) 推送develop代码到gitlab
(2) 查看流水线
(3) 查看SonarQube
3.5 远程部署
3.5.1 环境概述
(1) jenkins服务、docker服务
#Jenkins
192.168.38.45
#Dokcer
192.168.38.40
(2) 配置jenkins服务免密登录docker服务
#把Jenkins公钥发送到Dokcer服务
ssh-copy-id 192.168.38.40
(3) 配置 Publish over SSH
3.5.2 生成流水线脚本
sshPublisher(publishers: [sshPublisherDesc(configName: 'docker_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
3.5.3 Dokcer服务器部署脚本
#! /bin/sh
#接收外部参数,harbor地址、仓库名、项目名称、tag、端口
harbor_url=$1
harbor_repository=$2
project_name=$3
tag=$4
port=$5
imageName=$harbor_url/$harbor_repository/$project_name:$tag
echo "$imageName"
#查询容器是否存在,存在则删除
containerId=`docker ps -a | grep -w ${project_name}:${tag} | awk '{print $1}'`
if [ "$containerId" != "" ] ; then
#停掉容器
docker stop $containerId
#删除容器
docker rm $containerId
echo "成功删除容器"
fi
#查询镜像是否存在,存在则删除
imageId=`docker images | grep -w $project_name | awk '{print $3}'`
if [ "$imageId" != "" ] ; then
#删除镜像
docker rmi -f $imageId
echo "成功删除镜像"
fi
# 登录Harbor
docker login -u rosh -p Aa123456 $harbor_url
# 下载镜像
docker pull $imageName
# 启动容器,挂载日志
docker run -di -p $port:$port -v /usr/local/$harbor_repository/$project_name/logs:$project_name/logs $imageName
echo "容器启动成功"
3.5.4 修改Jenkinsfile
//=================定义公用信息===============
//harbor地址
def harborUrl = "192.168.38.100"
//harbor仓库名称
def harborRepository = "springcloud"
//构建版本tag
def tag = "latest"
//harbor凭证
def harborAuth = "dd671782-a857-4884-8307-f1084aef867d"
//定义函数,获取有效的服务
def getValidServerList(){
//需要构建的服务名
def buildServerNames = ['gateway-server','student-server']
//所有服务列表
def springCloudServers= [['serverName':'gateway-server','serverPort':9000],
['serverName':'student-server','serverPort':8001],
['serverName':'teacher-server','serverPort':8002]]
//返回结果集
def rs=[];
for(int i=0;i<buildServerNames.size();i++){
for(int j=0;j<springCloudServers.size();j++){
if("${buildServerNames[i]}" == "${springCloudServers[j].serverName}"){
rs.add(springCloudServers[j])
}
}
}
return rs;
}
node {
//有效的服务列表
def validServerList=getValidServerList()
stage('拉取代码') {
checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '0543fa76-913f-4600-9a3b-30ac70c0fc86', url: 'git@192.168.38.45:root/springcloud-demo.git']]])
}
stage('制作镜像,上传harbor'){
for(int i=0;i<validServerList.size();i++){
echo "制作镜像: ${validServerList[i].serverName}"
//服务名称
def serverName = "${validServerList[i].serverName}"
//每次构建前,清除上次构建的镜像
sh "/usr/local/shell/jenkins/clearImages.sh ${serverName}"
//打包,构建镜像
sh "mvn -f ${serverName} clean package dockerfile:build"
// 定义镜像名称
def imageName="${serverName}:${tag}"
// 打tag
sh "docker tag ${imageName} ${harborUrl}/${harborRepository}/${imageName}"
// 上传镜像
withCredentials([usernamePassword(credentialsId: "${harborAuth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harborUrl}"
//上传
sh "docker push ${harborUrl}/${harborRepository}/${imageName}"
}
}
}
stage('代码审查'){
//定义当前Jenkins的SonarQubeScanner工具
def scannerHome = tool 'sonarqube-scanner'
for(int i=0;i<validServerList.size();i++){
//引用当前JenkinsSonarQube环境
withSonarQubeEnv('sonarqube') {
sh """cd ${validServerList[i].serverName}
${scannerHome}/bin/sonar-scanner"""
}
}
}
stage('远程部署'){
for(int i=0;i<validServerList.size();i++){
sshPublisher(publishers: [sshPublisherDesc(configName: 'docker_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/usr/local/shell/jenkins/springcloudDeploy.sh ${harborUrl} ${harborRepository} ${validServerList[i].serverName} ${tag} ${validServerList[i].serverPort}", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}
}
3.5.5 集成测试
(1) 推送develop分支代码到gitlab
(2) 查看流水线
(3) 查看nacos
(4) 查看docker服务器
(5) 测试url
3.6 最终测试
3.6.1 部署全部服务
(1) 修改Jenkinsfile
(2) 推送develop分支到gitlab(3) 查看流水线
(4) 查看nacos
(5) 访问接口
3.6.2 部署单个服务
(1) 修改Jenkinsfile
(2) 修改老师接口
(3) 推送develop分支代码到gitlab(4) 查看流水线
(5) 再次访问老师接口
PS: 完整代码地址
https://github.com/zhurongsheng666/springcloud-ci.git