基于 Jenkins+Docker+SpringCloud微服务持续集成 作优化
上面部署方案存在的问题:
服务器列表
服务器名称 | IP地址 | 安装软件 | 硬件配置 | 系统 |
---|---|---|---|---|
代码托管服务器 | 192.168.100.240 | Gitlab-12.9.9 | 2核4G | CentOS Linux release 7.5.1804 |
持续集成服务器 | 192.168.100.241 | Jenkins 2.401.2,JDK 11,JDK 1.8,Maven 3.8.8,Git 1.8.3.1,Docker 20.10.24-ce | 2核4G | CentOS Linux release 7.5.1804 |
代码审查服务器 | 192.168.100.242 | mysql 5.7.43,sonarqube 6.7.7 | 1核2G | CentOS Linux release 7.5.1804 |
Harbor仓库服务器 | 192.168.100.251 | Docker 20.10.24-ce,Harbor 1.9.2 | 1核2G | CentOS Linux release 7.5.1804 |
生产部署服务器 | 192.168.100.252 | Docker 20.10.24-ce | 1核2G | CentOS Linux release 7.5.1804 |
生产部署服务器 | 192.168.100.253 | Docker 20.10.24-ce | 1核2G | CentOS Linux release 7.5.1804 |
eureka配置
让eureka集群实现一个互相注册的功能!
# 集群版
spring:
application:
name: EUREKA-HA
---
server:
port: 10086
spring:
# 指定profile=eureka-server1
profiles: eureka-server1
eureka:
instance:
# 指定当profile=eureka-server1时,主机名是eureka-server1
hostname: 192.168.100.252
client:
service-url:
# 将自己注册到eureka-server1、eureka-server2这个Eureka上面去
defaultZone: http://192.168.100.252:10086/eureka,http://192.168.100.253:10086/eureka
---
server:
port: 10086
spring:
profiles: eureka-server2
eureka:
instance:
hostname: 192.168.100.253
client:
service-url:
defaultZone: http://192.168.100.252:10086/eureka,http://192.168.100.253:10086/eureka
在启动微服务的时候,加入参数:spring.profiles.active
来读取对应的配置
其他微服务配置
除了Eureka注册中心以外,其他微服务配置都需要加入所有Eureka服务
# Eureka配置
eureka:
client:
service-url:
defaultZone: http://192.168.100.252:10086/eureka,http://192.168.100.253:10086/eureka # Eureka访问地址
instance:
prefer-ip-address: true
tensquare_zuul
配置
tensquare_admin_service
配置
tensquare_gathering
配置
把代码提交到Gitlab中
因为要进行多项选择,安装Extended Choice Parameter
插件
创建流水线项目(集群版)
添加参数化构建
选择字符串参数
添加多选项目参数
使用jdk1.8
构建项目
点击构建查看效果
// 定义变量以及引用变量,这样维护性好一点
// 引用凭证ID最好使用双引号 ""
// git凭证ID
def git_auth = "a4d0066f-6c58-4ab0-b714-6a24b3f5bc90"
// git的URL地址
def git_url = "[email protected]:tensquare_group/tensquare_back.git"
// 镜像的版本号
def tag = "latest"
// Harbor的url地址
def harbor_url = "192.168.100.251:85"
// 镜像库项目名称
def harbor_project = "tensquare"
// Harbor的登录凭证ID
def harbor_auth = "5785fbf3-a0f0-4234-8961-c866ca1e7046"
node {
// 获取当前选择的项目名称 ,调用split方法 以逗号切割项目名称 返回的变量是一个数组
def selectedProjectNames = "${project_name}".split(",")
stage('拉取代码') {
checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
}
stage('代码审查') {
//
for(int i=0;i<selectedProjectNames.length;i++){
// 遍历selectedProjectNames后取出每个元素里的内容,就是project_name 项目信息
def projectInfo = selectedProjectNames[i];
// 取出项目名字 , 以@符号切割 [0] 为第一个参数 tensquare_eureka
def currentProjectName = "${projectInfo}".split("@")[0]
// 当前遍历的项目端口,取出项目端口
def currentProjectPort = "${projectInfo}".split("@")[1]
// 定义当前Jenkins的SonarQubeScanner工具的环境,位于全局工具配置的SonarQube Scanner 的name定义为'sonar-scanner'
def scannerHome = tool 'sonar-scanner'
// 引用当前JenkinsSonarQube的环境,系统配置的SonarQube servers定义的name以及代码审查服务器位置
withSonarQubeEnv('sonarqube') {
sh """
cd ${currentProjectName}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
stage('编译安装公共子工程') {
sh "mvn -f tensquare_common clean install"
}
stage('编译打包微服务工程,上传镜像') {
// 项目参数传入的project_name 修改为变量
sh "mvn -f ${project_name} clean package dockerfile:build"
// 定义镜像名称
def imageName = "${project_name}:${tag}"
// 对镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"
// 把镜像推送到Harbor,项目为私有级别,登录时涉及一个问题,登录harbor需要输入账号和密码,Jenkinsfile需要配置项目目录下,会暴露给所有开发人员,为了安全使用凭证方式
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
// 登录到Harbor,引用如上定义的username和password,就是用户haibo的信息
sh "docker login -u ${username} -p ${password} ${harbor_url}"
// 镜像上传
sh "docker push ${harbor_url}/${harbor_project}/${imageName}"
sh "echo 镜像上传成功"
}
// 部署应用
sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project $project_name $tag $port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}
尝试多选项目进行构建
控制台输出能够看到进行了两次代码审查,证明Jenkinsfile能够对项目进行遍历审查
但构建结果是失败的,在编译打包的时候无法进行遍历与project_name
切割
修改Jenkinsfile
,注释掉远程发布,查看编译构建和打包效果
// git凭证ID
def git_auth = "a4d0066f-6c58-4ab0-b714-6a24b3f5bc90"
// git的URL地址
def git_url = "[email protected]:tensquare_group/tensquare_back.git"
// 镜像的版本号
def tag = "latest"
// Harbor的url地址
def harbor_url = "192.168.100.251:85"
// 镜像库项目名称
def harbor_project = "tensquare"
// Harbor的登录凭证ID
def harbor_auth = "5785fbf3-a0f0-4234-8961-c866ca1e7046"
node {
// 获取当前选择的项目名称 ,调用split方法 以逗号切割项目名称 返回的变量是一个数组
def selectedProjectNames = "${project_name}".split(",")
stage('拉取代码') {
checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
}
stage('代码审查') {
//
for(int i=0;i<selectedProjectNames.length;i++){
// 遍历selectedProjectNames后取出每个元素里的内容,就是project_name 项目信息
def projectInfo = selectedProjectNames[i];
// 取出项目名字 , 以@符号切割 [0] 为第一个参数 tensquare_eureka
def currentProjectName = "${projectInfo}".split("@")[0]
// 当前遍历的项目端口,取出项目端口
def currentProjectPort = "${projectInfo}".split("@")[1]
// 定义当前Jenkins的SonarQubeScanner工具的环境,位于全局工具配置的SonarQube Scanner 的name定义为'sonar-scanner'
def scannerHome = tool 'sonar-scanner'
// 引用当前JenkinsSonarQube的环境,系统配置的SonarQube servers定义的name以及代码审查服务器位置
withSonarQubeEnv('sonarqube') {
sh """
cd ${currentProjectName}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
stage('编译安装公共子工程') {
sh "mvn -f tensquare_common clean install"
}
stage('编译打包微服务工程,上传镜像') {
for(int i=0;i<selectedProjectNames.length;i++){
// 遍历selectedProjectNames后取出每个元素里的内容,就是project_name 项目信息
def projectInfo = selectedProjectNames[i];
// 取出项目名字 , 以@符号切割 [0] 为第一个参数 tensquare_eureka
def currentProjectName = "${projectInfo}".split("@")[0]
// 当前遍历的项目端口,取出项目端口
def currentProjectPort = "${projectInfo}".split("@")[1]
// 项目参数传入的project_name 修改为变量
sh "mvn -f ${currentProjectName} clean package dockerfile:build"
// 定义镜像名称
def imageName = "${currentProjectName}:${tag}"
// 对镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"
// 把镜像推送到Harbor,项目为私有级别,登录时涉及一个问题,登录harbor需要输入账号和密码,Jenkinsfile需要配置项目目录下,会暴露给所有开发人员,为了安全使用凭证方式
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
// 登录到Harbor,引用如上定义的username和password,就是用户haibo的信息
sh "docker login -u ${username} -p ${password} ${harbor_url}"
// 镜像上传
sh "docker push ${harbor_url}/${harbor_project}/${imageName}"
sh "echo 镜像上传成功"
}
// 部署应用
//sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deploy.sh $harbor_url $harbor_project $project_name $tag $port", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
}
}
此时可以发现两个服务都可以进行代码扫描以及进行构建打包和上传至镜像仓库!
配置第二台生产服务器 192.168.100.253
# 卸载旧版本
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
# 删除docker的所有镜像和容器
rm -rf /var/lib/docker
# 安装基本的依赖包
sudo yum install yum-utils device-mapper-persistent-data lvm2 -y
# 设置镜像仓库 Docker yum源
sudo yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 更新yum软件包索引
sudo yum makecache fast
# 列出需要安装的版本列表
yum list docker-ce --showduplicates | sort -r
# 安装docker-ce-20.10
yum install docker-ce-20.10.* docker-ce-cli-20.10.* containerd -y
# 配置Docker镜像仓库加速以及配置Harbor镜像仓库信任
mkdir /etc/docker -p
cat > /etc/docker/daemon.json <<EOF
{
"registry-mirrors": ["https://k68iw3ol.mirror.aliyuncs.com"],
"insecure-registries": ["192.168.100.251:85"]
}
EOF
# 开启内核转发,后续报错排查后回来整理的笔记,重要!!!
vim /etc/sysctl.conf
net.ipv4.ip_forward = 1
# 重装使配置生效
sysctl -p /etc/sysctl.conf
systemctl daemon-reload && systemctl enable --now docker
Jenkins服务器拷贝公钥到远程生产服务器02
ssh-copy-id 192.168.100.253
系统配置->添加远程服务器
Publish over SSH–> 新增一台机器
项目配置添加参数
配置能够选择多个服务器
返回项目构建查看效果
更改Jenkinsfile
添加遍历服务器启动容器配置
// git凭证ID
def git_auth = "a4d0066f-6c58-4ab0-b714-6a24b3f5bc90"
// git的URL地址
def git_url = "[email protected]:tensquare_group/tensquare_back.git"
// 镜像的版本号
def tag = "latest"
// Harbor的url地址
def harbor_url = "192.168.100.251:85"
// 镜像库项目名称
def harbor_project = "tensquare"
// Harbor的登录凭证ID
def harbor_auth = "5785fbf3-a0f0-4234-8961-c866ca1e7046"
node {
// 获取当前选择的项目名称,调用split方法切割逗号,意为切开项目名称
def selectedProjectNames = "${project_name}".split(",")
// 获取当前选择的服务器名称
def selectedServers = "${publish_server}".split(",")
stage('拉取代码') {
checkout scmGit(branches: [[name: "*/${branch}"]], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]])
}
stage('代码审查') {
// 对项目遍历进行代码审查,定义变量i=0 遍历数组selectedProjectNames 数组长度为length
for(int i=0;i<selectedProjectNames.length;i++){
// 取出每个元素里的内容 定义变量为projectInfo 就是项目信息,即包含项目名字也包含项目端口
def projectInfo = selectedProjectNames[i];
// 对 projectInfo 进行切割 引用projectInfo 变量,使用@符号切,当前遍历的项目名字 [0] 为获取的第一个元素 就是项目名字
def currentProjectName = "${projectInfo}".split("@")[0]
// 当前遍历的项目端口 [1] 就是获取的第二个元素
def currentProjectPort = "${projectInfo}".split("@")[1]
// 定义当前Jenkins的SonarQubeScanner工具的环境,位于全局工具配置的SonarQube Scanner 的name定义为'sonar-scanner'
def scannerHome = tool 'sonar-scanner'
// 引用当前JenkinsSonarQube的环境,系统配置的SonarQube servers定义的name以及代码审查服务器位置
withSonarQubeEnv('sonarqube') {
sh """
cd ${currentProjectName}
${scannerHome}/bin/sonar-scanner
"""
}
}
}
stage('编译安装公共子工程') {
sh "mvn -f tensquare_common clean install"
}
stage('编译打包微服务工程,上传镜像') {
for(int i=0;i<selectedProjectNames.length;i++){
// 取出每个元素里的内容 定义变量为projectInfo 就是项目信息,即包含项目名字也包含项目端口
def projectInfo = selectedProjectNames[i];
// 对 projectInfo 进行切割 引用projectInfo 变量,使用@符号切,当前遍历的项目名字 [0] 为获取的第一个元素 就是项目名字
def currentProjectName = "${projectInfo}".split("@")[0]
// 当前遍历的项目端口 [1] 就是获取的第二个元素
def currentProjectPort = "${projectInfo}".split("@")[1]
// 项目参数传入的project_name 修改为变量
sh "mvn -f ${currentProjectName} clean package dockerfile:build"
// 定义镜像名称
def imageName = "${currentProjectName}:${tag}"
// 对镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project}/${imageName}"
// 把镜像推送到Harbor,项目为私有级别,登录时涉及一个问题,登录harbor需要输入账号和密码,Jenkinsfile需要配置项目目录下,会暴露给所有开发人员,为了安全使用凭证方式
withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
// 登录到Harbor,引用如上定义的username和password,就是用户haibo的信息
sh "docker login -u ${username} -p ${password} ${harbor_url}"
// 镜像上传
sh "docker push ${harbor_url}/${harbor_project}/${imageName}"
sh "echo 镜像上传成功"
}
// 遍历所有的服务器,分别部署
for(int j=0;j<selectedServers.length;j++){
// 获取当前遍历的服务器名称
def currentServerName = selectedServers[j]
// 加上的参数格式:--spring.profiles.active=eureka-server1/eureka-server2
def activeProfile = "--spring.profiles.active="
// 根据不同的服务器名字来读取不同的eureka配置信息
if(currentServerName=="master_server"){
activeProfile = activeProfile+"eureka-server1"
}else if (currentServerName=="slave_server"){
activeProfile = activeProfile+"eureka-server2"
}
// 部署应用
sshPublisher(publishers: [sshPublisherDesc(configName: "${currentServerName}", transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "/opt/jenkins_shell/deployCluster.sh $harbor_url $harbor_project $currentProjectName $tag $currentProjectPort $activeProfile", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '', remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
}
echo "${currentProjectName}完成编译,构建镜像"
}
}
}
更改服务器端部署脚本
在deploy.sh
的基础上再编写一个deployCluster.sh
脚本
增加一个变量profile,用来存储新增的位置变量activeProfile,也是传参的$6
vim /opt/jenkins_shell/deployCluster.sh
#!/bin/sh
# 接收外部参数
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5
Profile=$6
imageName=$harbor_url/$harbor_project_name/$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 haibo -p LIUhaibo123 $harbor_url
# 下载镜像
docker pull $imageName
# 启动容器
docker run -di -p $port:$port $imageName $profile
echo "容器启动成功"
# 添加执行权限
chmod +x /opt/jenkins_shell/deployCluster.sh
尝试构建项目,部署eureka服务器至两台服务器上
eureka集群启动并且使用10086端口
部署剩下的微服务集群
四个微服务都部署完成后通过浏览器查看,并且所有微服务都已经成功注册到eureka服务器也已经形成集群形式
第一个报错:eureka服务器容器启动后端口为8080
容器启动后查看容器日志,发现eureka服务器监听端口为8080,心想我的配置文件设置为10086端口为什么启动后是监听8080端口?
重构Jenkinsfile文件的docker容器启动传参部分
// 加上的参数格式:--spring.profiles.active=eureka-server1/eureka-server2
def activeProfile = "--spring.profiles.active=" //添加等号 =
// 根据不同的服务器名字来读取不同的eureka配置信息
if(currentServerName=="master_server"){
activeProfile = activeProfile+"eureka-server1"
}else if (currentServerName=="slave_server"){
activeProfile = activeProfile+"eureka-server2"
}
于是手动启动容器传参后报错
docker run -di -p 10086:10086 2c37266049d5 --spring.profiles.activeeureka-server2
WARNING: IPv4 forwarding is disabled. Networking will not work.
# 警告:IPv4转发已禁用。网络将不起作用。
添加docker ipv4转发
vim /etc/sysctl.conf
net.ipv4.ip_forward = 1
# 重装使配置生效
sysctl -p /etc/sysctl.conf
# 重启docker
systemctl restart docker
启动后还是监听8080端口,尝试使用--spring.profiles.active=eureka-server2
进行传参后启动成功
docker run -di -p 10086:10086 2c37266049d5 --spring.profiles.active=eureka-server2
第二个报错:【已解决】com.mysql.jdbc.exceptions.jdbc4.CommunicationsExcepti:Communications link failure ----mysql连接报错
配置负载均衡池,里面地址就是zuul网关地址
proxy_pass
选择不同的zuulServer
# 位于pro02生产服务器进行配置
# 安装Nginx
# 修改Nginx配置
vim /etc/nginx/nginx.conf
# http模块添加
upstream zuulServer {
server 192.168.100.252:10020 weight=1;
server 192.168.100.253:10020 weight=1;
}
vim /etc/nginx/conf.d/default.conf
server {
listen 85;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
root /usr/share/nginx/html;
location / {
# root /usr/share/nginx/html;
# index index.html index.htm;
# 指定服务器负载均衡服务器
proxy_pass http://zuulServer/;
}
# 重启Nginx
systemctl restart nginx