前言
通过前几章的学习,了解了持续集成概念以及相关技术的学习和使用
接下来学习构建一整套SpringCloud微服务+Docker+Jenkins的持续集成案例
环境
架构图
流程说明:
- 开发人员将代码提交到代码托管平台,如Gitlab、Github
- Jenkins 从代码托管平台拉取代码、编译、打包后,通过Docker容器技术构建成镜像
- jenkins将构建好的镜像上传到远程容器管理平台Harbor
- Jenkins通过连接项目部署服务器,拉取Harbor上的项目镜像,创建容器,启动服务
- 用户远程访问部署服务器上的服务
服务列表
服务器名称 | IP地址 | 安装的软件 |
---|---|---|
代码托管服务器 | 192.168.2.217 | Gitlab |
持续集成服务器 | 192.168.2.244 | Jenkins、Maven、Docker |
Docker仓库服务器 | 192.168.2.217 | Docker Harbor |
项目部署服务器 | 192.168.2.219/192 | Docker |
微服务项目
项目技术
SpringCloud + SpringBoot +Mysql
微服务项目结构
- cloud2022 父工程
- cloud-api-common 项目公共类、通用代码
- cloud-consume-order80 消费者,通过注册中心调用后端微服务 ,端口为80
- cloud-eureka-server7001 Eureka技术,微服务的注册中心1, 端口为7001
- cloud-eureka-server7002 Eureka技术,微服务的注册中心2,端口为7002
- cloud-provide-payment8001 服务提供者1,端口为8001
- cloud-provide-payment8002 服务提供者2,端口为8002
数据库结构
项目部分代码
@RestController
@Slf4j
public class OrderController {
public static final String PAYENT_URL = "http://CLOUD-PAYMENT-SERVICE";
@Resource
private RestTemplate restTemplate;
// 注入自定义的负载均衡规则
@Resource
private MyLoadBlance myLoadBalancer;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping (value = "/consume/payment/create")
public CommonResult<Payment> create(Payment payment) {
return restTemplate.postForObject(PAYENT_URL+"/payment/create",payment,CommonResult.class);
}
@GetMapping(value = "/consume/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") long id) {
return restTemplate.getForObject(PAYENT_URL+"/payment/get/"+id,CommonResult.class);
}
/**
* 测试自定义负载均衡算法,测试时,需要取消 restTemplate配置类的注解 @LoadBalanced
* @LoadBalanced 让RestTemplate具备默认轮询的负载策略,所以会跟自定义负载策略冲突
* @return
*/
@GetMapping(value = "/consume/payment/lb")
public String getPaymentLB() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.isEmpty()) {
return null;
}
// 调用自定义的负载均衡策略
ServiceInstance serviceInstance = myLoadBalancer.instance(instances);
URI uri = serviceInstance.getUri();
System.out.println(uri.toString());
//http://192.168.109.1:8001/payment/lb
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
}
通过Http客户端工具RestTemplate工具访问Eureka注册中心的后端微服务(负载均衡)
实际测试接口
@GetMapping(value = "/consume/payment/get/{id}")
代码地址:
https://gitee.com/lucky_sungit/spring-cloud2022_jenkins
上述仓库是包含了后续的Jenkinsfile脚本以及shell脚本的完结版本的代码
本地部署
SpringCloud微服务部署
本地运行微服务
1)逐一启动微服务
2)使用 postman 测试功能是否可用
本地部署微服务
1)Springboot微服务项目打包
在每个微服务的pom.xml中导入打包插件
cloud-api-common无需配置该插件
<build>
<!-- 定义项目的上下文 -->
<finalName>cloud_80</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.2.1.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal><!--可以把依赖的包都打包到生成的Jar包中 -->
</goals>
</execution>
</executions>
</plugin>
</build>
打包
如果在父pom下打包,会报错,所以需要整个install
然后clean 、package即可全部打包,或者单个微服务下打包
测试
先启动注册中心集群,然后启动服务提供者集群,最后启动消费者服务,通过http访问接口即可
Docker仓库
docker介绍以及用法暂不介绍
Harbor
简介
Harbor(港口,港湾)是一个用于存储和分发Docker镜像的企业级Registry服务器。
除了Harbor这个私有镜像仓库之外,还有Docker官方提供的Registry。相对Registry,Harbor具有很多优势:
1).提供分层传输机制,优化网络传输 Docker镜像是是分层的,而如果每次传输都使用全量文件(所以用FTP的方式并不适合),显然不经济。必须提供识别分层传输的机制,以层的UUID为标识,确定 传输的对象。
2).提供WEB界面,优化用户体验 只用镜像的名字来进行上传下载显然很不方便,需要有一个用户界面可以支持登陆、搜索功能,包括区分公有、私有镜像。
3).支持水平扩展集群 当有用户对镜像的上传下载操作集中在某服务器,需要对相应的访问压力作分解。
4).良好的安全机制 企业中的开发团队有很多不同的职位,对于不同的职位人员,分配不同的权限, 具有更好的安全性。
安装
Harbor需要安装在192.168.2.219上面
1)先安装Docker并启动Docker(已完成) 参考之前的安装过程
2)再安装Docker-compose
一键安装脚本
#! /bin/bash
echo "---------开始安装---------"
sleep 1
echo "############判断是否安装了docker##############"
if ! type docker >/dev/null 2>&1; then
cat >/etc/yum.repos.d/docker.repo<<EOF
[docker-ce-edge]
name=Docker CE Edge - \$basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/7/\$basearch/edge
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg
EOF
echo 'docker 未安装';
echo '开始安装Docker....';
yum -y install docker-ce
echo '配置Docker开启启动';
systemctl enable docker
systemctl start docker
cat >> /etc/docker/daemon.json << EOF
{
"registry-mirrors": ["https://b9pmyelo.mirror.aliyuncs.com"]
}
EOF
systemctl restart docker
else
echo 'docker 已安装';
fi
echo "############判断是否安装了docker-compose##############"
if ! type docker-compose >/dev/null 2>&1; then
echo 'docker-compose 未安装';
echo '开始安装docker-compose....';
wget http://oss.moguit.cn/script//docker-compose-Linux-x86_64
mv docker-compose-Linux-x86_64 docker-compose
chmod +x docker-compose
mv docker-compose /usr/local/bin/
docker-compose -v
else
echo 'docker-compose 已安装';
fi
3)安装Harbor
下载安装包
https://github.com/goharbor/harbor/releases
上传服务器并解压
tar -xvf harbor-offline-installer-v1.9.2.tgz -C /opt
cd harbor
修改Harbor的配置
vi harbor.yml
hostname: 192.168.2.217
port:8888
安装Harbor
./prepare
/inistakk.sh
启动Harbor
docker-compose up -d 启动
docker-compose stop 停止
docker-compose restart 重启
访问Harbor
默认密码: admin Harbor12345 (可以在harbor.yml中修改)
操作
1) 创建项目
Harbor的项目分为公开和私有的:
公开项目:所有用户都可以访问,通常存放公共的镜像,默认有一个library公开项目。
私有项目:只有授权用户才可以访问,通常存放项目本身的镜像。
我们可以为微服务项目创建一个新的项目
2)创建用户
1) 给私有项目分配用户
进入cloud2022项目->成员
角色 | 权限说明 |
---|---|
访客 | 对于指定项目拥有只读权限 |
开发人员 | 对于指定项目拥有读写权限 |
维护人员 | 对于指定项目拥有读写权限,创建 Webhooks |
项目管理员 | 除了读写权限,同时拥有用户管理/镜像扫描等管理权限 |
测试
把镜像上传到Harbor
1)配置harbor信任列表
vi /etc/docker/daemon.json
{
"registry-mirrors": ["https://b9pmyelo.mirror.aliyuncs.com"],
"insecure-registries": ["192.168.2.217:8888"]
}
2)重启docker
systemctl restart docker
3)登陆harbor服务器
docker login -u admin -p harbor12345 http://192.168.2.217:8888
4)给镜像打上标签
docker tag eureka:v1 192.168.2.217:8888/cloud2022/eureka:v1
5) 推送镜像
docker push 192.168.2.217:8888/cloud2022/eureka:v1
6)拉取镜像
docker pull 192.168.2.217:8888/cloud2022/eureka:v1
7)创建实例
docker run -it -d --name=test =p 7001:7001 192.168.2.217:8888/cloud2022/eureka:v1
Jenkins持续集成
微服务项目上传到Gitlab
jenkins从Gitlab拉取源码
//gitlab的凭证
def jenkins_Id="b6ccfe64-2c07-4b33-a39c-beec91d4a026"
node{
stage('拉取代码'){
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${jenkins_Id}", url: "${git_url}"]]])
}
}
拉取jenkinsfile文件
提交到SonarQube代码审查
添加两个参数
每个项目的根目录下添加sonar-project.properties
sonar.projectKey=cloud-eureka-server7002
sonar.projectName=cloud-eureka-server7002
sonar.projectVersion=1.0
sonar.sources=.
sonar.exclusions=**/test/**,**/target/**
sonar.java.binaries=.
sonar.java.source=1.8
sonar.java.target=1.8
#sonar.java.libraries=**/target/classes/**
sonar.sourceEncoding=UTF-8
注意:修改sonar.projectKey和sonar.projectName
修改Jenkinsfile构建脚本
//gitlab的凭证
def jenkins_Id="b6ccfe64-2c07-4b33-a39c-beec91d4a026"
node{
stage('拉取代码'){
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${jenkins_Id}", url: "${git_url}"]]])
}
stage('代码审查'){
def scannerHome = tool 'sonarqube-scanner' //全局工具
withSonarQubeEnv('sonar-env'){ //全局环境变量
sh """
cd ${current_project_name}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
使用Dockerfile编译、生成镜像
利用dockerfile-maven-plugin插件构建Docker镜像
在每个微服务项目的pom.xml加入dockerfile-maven-plugin插件
<!--docker插件-->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<repository>${project.artifactId}</repository>
<!-- <tag>${project.parent.version}</tag>-->
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>//该参数对应dockefile的 ARG
</buildArgs>
</configuration>
</plugin>
在每个微服务项目根目录下建立Dockerfile文件
#FROMjava:8
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
EXPOSE 7002
ENTRYPOINT ["java","-jar","/app.jar"]
修改Jenkinsfile构建脚本
//gitlab的凭证
def jenkins_Id="b6ccfe64-2c07-4b33-a39c-beec91d4a026"
//定义上传的镜像标签
def tag="latest"
node{
stage('拉取代码'){
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${jenkins_Id}", url: "${git_url}"]]])
}
stage('代码审查'){
def scannerHome = tool 'sonarqube-scanner' //全局工具
withSonarQubeEnv('sonar-env'){ //全局环境变量
sh """
cd ${current_project_name}
${scannerHome}/bin/sonar-scanner
"""
}
}
stage("安装父工程"){//找不到父工程依赖,需要将父工程打入仓库
sh "mvn clean install"
}
stage("公共子服务安装"){
sh "mvn -f cloud-api-common clean install"
}
stage("构建镜像"){
//定义镜像标签
def imageName="${project_name}:${tag}"
sh "mvn -f ${project_name} clean package dockerfile:build"
}
}
注意:步骤{安装父工程} 如果出现找不到父工程依赖,需要手动把父工程的依赖上传到仓库中
上传到Harbor镜像仓库
//gitlab的凭证
def jenkins_Id="b6ccfe64-2c07-4b33-a39c-beec91d4a026"
def git_url="http://192.168.2.217:8444/CICD/springcloud2022.git"
//Harbor的凭证
def harbor_url="192.168.2.217:8888"
//定义上传的镜像标签
def tag="latest"
//Harbor项目名称
def harbor_project_name="cloud2022"
//harbor的凭证
def harbor_auth="23d2b1c5-941f-4144-8aef-3228b11f24a7"
node{
stage('拉取代码'){
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${jenkins_Id}", url: "${git_url}"]]])
}
stage('代码审查'){
def scannerHome = tool 'sonarqube-scanner' //全局工具
withSonarQubeEnv('sonar-env'){ //全局环境变量
sh """
cd ${current_project_name}
${scannerHome}/bin/sonar-scanner
"""
}
}
stage("安装父工程"){//找不到父工程依赖,需要将父工程打入仓库
sh "mvn clean install"
}
stage("公共子服务安装"){
sh "mvn -f cloud-api-common clean install"
}
stage("构建镜像"){
//定义镜像标签
def imageName="${project_name}:${tag}"
sh "mvn -f ${project_name} clean package dockerfile:build"
//给本地镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName} "
//登陆Harbor,并上传镜像到仓库
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登陆Harbor
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//并上传镜像
sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"
}
//删除本地镜像
sh "docker rmi ${imageName}"
sh "docker rmi ${harbor_url}/${harbor_project_name}/${imageName}"
}
}
使用凭证管理Harbor私服账户和密码
先在凭证建立Harbor的凭证,在生成凭证脚本代码
拉取镜像和发布应用
结构
注意:192.168.66.103服务已经安装Docker并启动安装 Publish Over SSH 插件
安装以下插件,可以实现远程发送Shell命令
配置远程部署服务器
拷贝公钥到远程服务器
ssh-copy-id 192.168.2.192
系统配置->添加远程服务器
如果path to key报错的话,填入jenkins所在主机的key
修改Jenkinsfile构建脚本
添加一个port参数
stage {构建镜像步骤} 添加模板代码
sshPublisher(publishers: [sshPublisherDesc(configName: "${host}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $project_name $project_name $tag $port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
编写deploy.sh部署脚本
#!/bin/sh
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5
imagename=$harbor_url/$harbor_project_name/$project_name:$tag
##判断本地是都有容器
container_id=`docker ps -qa | grep $project_name | awk '{print$1}'`
if [ $container_id != "" ];then
echo "容器存在,即将删除"
docker rm -f $container_id
fi
##判断镜像是否存在
image_id=`docker images | grep $project_name | awk '{print$3}'`
if [ $image_id] != "" ];then
echo "删除镜像"
docker rmi $imagename
fi
##登陆Harbor
docker login -u admin -p Harbor12345 $harbor_url
##拉取镜像
docker pull $imagename
##启动实例
docker run -it -d --name=$project_name -p $port:$port $imagename
echo "容器启动成功"
上传deploy.sh文件到/opt/jenkins_shell目录下,且文件至少有执行权限!
chmod +x deploy.sh 添加执行权限
测试
部署方案优化
上面部署方案存在的问题:
1) 一次只能选择一个微服务部署
2) 只有一台生产者部署服务器
3) 每个微服务只有一个实例,容错率低
优化方案:
1) 在一个Jenkins工程中可以选择多个微服务同时发布
2) 在一个Jenkins工程中可以选择多台生产服务器同时部署
3) 每个微服务都是以集群高可用形式部署
设计Jenkins集群项目的构建参数
1) 安装Extended Choice Parameter插件支持多选框
2)添加参数
字符串参数:分支
多选框:项目名称
注意:不填的话,默认‘,’ 区分中文和英文
//gitlab的凭证
def jenkins_Id="b6ccfe64-2c07-4b33-a39c-beec91d4a026"
def git_url="http://192.168.2.217:8444/CICD/springcloud2022.git"
//Harbor的凭证
def harbor_url="192.168.2.217:8888"
//定义上传的镜像标签
def tag="latest"
//Harbor项目名称
def harbor_project_name="cloud2022"
//harbor的凭证
def harbor_auth="23d2b1c5-941f-4144-8aef-3228b11f24a7"
node{
//项目名称数组
def selectedProjects="${project_name}".split(',')
stage('拉取代码'){
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${jenkins_Id}", url: "${git_url}"]]])
}
stage('代码审查'){
for(int i = 0;i < selectedProjects.length;i++){
//取出每个项目的名称和端口
def project_info = selectedProjects[i]
//项目名称
def current_project_name = "${project_info}".split('@')[0]
def scannerHome = tool 'sonarqube-scanner'
withSonarQubeEnv('sonar-env'){
sh """
cd ${current_project_name}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
stage("安装父工程"){//找不到父工程依赖,需要将父工程打入仓库
sh "mvn clean install"
}
stage("公共子服务安装"){
sh "mvn -f cloud-api-common clean install"
}
stage("编译、构建本地镜像"){
for(int j = 0; j < selectedProjects.length; j++){
def current_project = selectedProjects[j]
def current_project_name = "${current_project}".split("@")[0]
def current_port = "${current_project}".split("@")[1]
//定义镜像标签
def imageName="${current_project_name}:${tag}"
sh "mvn -f ${current_project_name} clean package dockerfile:build"
//给本地镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName} "
//登陆Harbor,并上传镜像到仓库
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登陆Harbor
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//并上传镜像
sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"
}
//删除本地镜像
sh "docker rmi ${imageName}"
sh "docker rmi ${harbor_url}/${harbor_project_name}/${imageName}"
//远程调用
sshPublisher(publishers: [sshPublisherDesc(configName: 'master', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project_name $current_project_name $tag $current_port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
echo "${current_project_name}完成编译,构建镜像"
}
}
}
}
完成微服务多服务器远程发布
1) 配置远程部署服务器
拷贝公钥到远程服务器
ssh-copy-id 192.168.66.104
2)系统配置->添加远程服务器
3)修改Docker配置信任Harbor私服地址 重启Docker
4)添加参数
多选框:部署服务器
最终效果
5) 修改Jenkinsfile构建脚本
//gitlab的凭证
def jenkins_Id="b6ccfe64-2c07-4b33-a39c-beec91d4a026"
def git_url="http://192.168.2.217:8444/CICD/springcloud2022.git"
//Harbor的凭证
def harbor_url="192.168.2.217:8888"
//定义上传的镜像标签
def tag="latest"
//Harbor项目名称
def harbor_project_name="cloud2022"
//harbor的凭证
def harbor_auth="23d2b1c5-941f-4144-8aef-3228b11f24a7"
node{
//项目名称数组
def selectedProjects="${project_name}".split(',')
//定义主机
def servers="${server}".split(',')
stage('拉取代码'){
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${jenkins_Id}", url: "${git_url}"]]])
}
stage('代码审查'){
for(int i = 0;i < selectedProjects.length;i++){
//取出每个项目的名称和端口
def project_info = selectedProjects[i]
//项目名称
def current_project_name = "${project_info}".split('@')[0]
def scannerHome = tool 'sonarqube-scanner'
withSonarQubeEnv('sonar-env'){
sh """
cd ${current_project_name}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
stage("安装父工程"){//找不到父工程依赖,需要将父工程打入仓库
sh "mvn clean install"
}
stage("公共子服务安装"){
sh "mvn -f cloud-api-common clean install"
}
stage("编译、构建本地镜像"){
for(int j = 0; j < selectedProjects.length; j++){
def current_project = selectedProjects[j]
def current_project_name = "${current_project}".split("@")[0]
def current_port = "${current_project}".split("@")[1]
//定义镜像标签
def imageName="${current_project_name}:${tag}"
sh "mvn -f ${current_project_name} clean package dockerfile:build"
//给本地镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName} "
//登陆Harbor,并上传镜像到仓库
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登陆Harbor
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//并上传镜像
sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"
}
//删除本地镜像
sh "docker rmi ${imageName}"
sh "docker rmi ${harbor_url}/${harbor_project_name}/${imageName}"
for(int i = 0; i < servers.length; i++){
def host = servers[i]
echo host
//远程调用
sshPublisher(publishers: [sshPublisherDesc(configName: "${host}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project_name $current_project_name $tag $current_port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
echo "${current_project_name}完成编译,构建镜像"
}
}
}
}
注意:多台部署服务器都需要创建目录以及脚本文件
/opt/jenkins_shell/deploy.sh
最终测试