本笔记内容是 2018 年 ~ 2020 年的一个实际产物,如今有场景需要再次用到这里的部署方案,就找了下之前的脚本总结成了该文章 该脚本参考资料:
- 基础知识:鸟哥私房菜-基础版- 学习笔记
- 有了基础知识之后,按照流程逻辑,遇到不会的就需要去百度查询写法,比如 awk 如何提取文本信息成数组显示之类的
推荐一个 IDEA 的 shell 检查插件,Shell Script强烈推荐,idea 官方出品,里面有一个扩展 shellcheck,在你打开 sh 脚本的时候会提示你是否安装它,这个很有用,会告诉你一些写法可能是有问题的,然后可以根据提示(一般有问题的会有黄色的背景)你可以去看看为什么会有提示,要如何修改。这个功能还挺不错的,强烈推荐
该方案前提:
- jar 包运行机器和 jar 包打包的机器可以是同一个,也可以是不同的机器(不同机器使用 ssh 命令去执行命令,提前配置好 ssh 免密登录 )
- 需要在打包机器上安装 git、gradle 环境;因为需要使用命令来模拟人工打包部署的流程
功能描述:
- 使用 git pull 更新代码
- gradle 打包脚本
- 将打包好的 jar 包复制到指定位置
- 按 jar 包名称查找进程,并使用 kill -9 暴力杀掉进程
- 运行 jar 包
java 启动 jar 包命令
nohup java -jar 微服务j.jar --eureka.instance.hostname=内网ip --spring.profiles.active=prod > ./catalina.out 2>&1 &
# 服务注册中心需要额外指定一个参数
--eureka.environment=eureka-client-prod
# 如果有日志框架输出了日志文件,可以将后面控制台输出到 /dev/null 中
nohup java -jar 微服务j.jar > /dev/null 2>&1 &
打包脚本 - v1 基础版
这里分两个操作脚本:打包、杀掉进程和运行
脚本单独存放,比如存放在 /data/packaging/script
build.sh 打包脚本
#!/bin/sh
# 存放 git 代码的分类仓库,有多个项目的话,会放到同一个 目录下
BASE_DIR=/data/packaging/git-code
# git 仓库目录名称,在 BASE_DIR 下的目录名称
PROJECT=app1
# 要部署的 jar 包目录
APP_DIR=/data/service/app1
# 远程服务器的 ip,或则实现配置免密登录的 host 名称
IP=config
echo -e "bootjar ${PROJECT}"
cd $BASE_DIR/${PROJECT}
git pull
# -PbuildProfile=prod 是 gradle 的环境变量方式,没有使用到该方式的可以不用这个
# gradle -PbuildProfile=prod bootJar
gradle bootJar
# 如果目标机器上的部署目录不存在,就创建
# 这里的 IP 如果登录目标机器是指定账户,这可以使用 用户名@IP 的方式执行
if ssh $IP test -e $APP_DIR;
then echo $filePathexists
else ssh $IP "mkdir ${APP_DIR}"
fi
# 将打包好的 jar 包复制到目标机器
scp -r ./build/libs/* $IP:$APP_DIR
kill-run.sh 杀掉进程和启动 jar 包
#!/bin/sh
APP_DIR=/data/service/app1
APP_JAR=app-1.0.0-boot.jar
IP_ADDRESS=192.168.3.76
ssh config "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"
ssh config "cd ${APP_DIR}; source /etc/profile; nohup java -jar ${APP_JAR} --eureka.instance.ip-address=${IP_ADDRESS} --spring.profiles.active=prod > /dev/null 2>&1 &"
打包脚本 - v2 版
v1 版本经过一段使用之后,发现了一些不太方便的场景:
- 当 jar 包版本号发生改变的时候打包发布变得困难,需要修改发布脚本,手动执行 kill 命令等操作
- 多开实例一个 boot.jar 往多个服务器推送的时候,变得困难,需要 copy 多次代码
v2 脚本重构目的:脚本顶部提取变量,其他部分全部一样。维护变得简单
build.sh
#!/bin/sh
BASE_DIR=/data/packaging/git-code
PROJECT=app1
APP_DIR=/data/service/$PROJECT
# 推送目标机器,多个空格隔开
PUSH_TARGET_HOST_NAMES="cloud-04"
echo -e "${PROJECT} ..."
cd $BASE_DIR/${PROJECT}
git pull
rm -rf ./build/libs/*
gradle -PbuildProfile=prod bootJar
# 其他脚本中统一使用一个 jar 名称,防止版本升级脚本改动
mv ./build/libs/*-boot.jar ./build/libs/$PROJECT-boot.jar
for hostName in $PUSH_TARGET_HOST_NAMES ; do
if ssh $hostName test -e $APP_DIR;
then echo $filePathexists
else ssh $hostName "mkdir ${APP_DIR}"
fi
scp -r ./build/libs/*-boot.jar $hostName:$APP_DIR
done
kill.sh
#!/bin/sh
APP_JAR=app-boot.jar
TARGET_HOST_NAME=cloud-04
ssh $TARGET_HOST_NAME "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"
kill-run.sh
#!/bin/sh
APP_DIR=/data/service/app1
APP_JAR=app1-boot.jar
IP_ADDRESS=192.168.7.2
TARGET_HOST_NAME=cloud-04
ssh $TARGET_HOST_NAME "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"
ssh $TARGET_HOST_NAME "cd ${APP_DIR}; source /etc/profile; nohup java -jar ${APP_JAR} --eureka.instance.ip-address=${IP_ADDRESS} --spring.profiles.active=prod > /dev/null 2>&1 &"
如果有多个实例,kill 与 kill-run 脚本是分开写的。为什么呢?因为往往可能只是想重启其中一台的,而且这种几率非常大
打包脚本 - v3 交互版
该脚本比较复杂,先来看看交互界面,然后再看脚本
start.sh 交互脚本核心
#!/bin/sh
# Program:
# cloud 微服务统一部署,提供打包推送、kill、run、检查项目是否运行等功能
# History:
# 2020/02/18 mrcode first relese
# 2020/02/28 mrcode 修复多机器打包机器选择逻辑错误,函数内部变量跨域等问题
SH_VERSION="v0.4"
# 打包项目并推送到目标机器
function f_pack() {
local _uiName=$uiName
local _BASE_DIR=$BASE_DIR
local _PROJECT=$PROJECT
local _APP_DIR=$APP_DIR
local _PUSH_TARGET_HOST_NAMES=$PUSH_TARGET_HOST_NAMES
echo -e "\n开始部署项目 ${_PROJECT} ..."
cd ${_BASE_DIR}/${_PROJECT}
git pull
rm -rf ./build/libs/*
gradle -PbuildProfile=prod bootJar
# 其他脚本中统一使用一个 jar 名称,防止版本升级脚本改动
mv ./build/libs/*-boot.jar ./build/libs/${_PROJECT}-boot.jar
for hostName in ${_PUSH_TARGET_HOST_NAMES} ; do
if ssh $hostName test -e ${_APP_DIR}; then
echo $filePathexists
else
ssh $hostName "mkdir ${_APP_DIR}"
fi
scp -r ./build/libs/*-boot.jar $hostName:${_APP_DIR}
done
}
function f_checkIsRun() {
local checkIsRunInfo=$(ssh $TARGET_HOST_NAME "ps -ef | grep root | grep $1")
if [ "$checkIsRunInfo" == "" ]; then
echo "未运行"
else
echo $checkIsRunInfo
fi
}
# 打印 host 待选菜单,并完成选择
function f_selectHostName() {
local index=0
echo -e "\n当前项目有如下多个实例机器需要部署:"
for i in ${hostNames[*]} ; do
index=$((index+1))
echo "$index. $i"
done
echo -e "\n"
read -p "请选择要操作的机器:" hostNameN
TARGET_HOST_NAME=${hostNames[$(($hostNameN-1))]}
echo -e "当前选择操作项目为 $PROJECT $TARGET_HOST_NAME \n"
}
# 检测是否有多台机器
function f_checkSelectHostName() {
local hostNames=($PUSH_TARGET_HOST_NAMES)
if [ "${#hostNames[@]}" -gt 1 ]; then
# 选择哪一台机器
f_selectHostName
fi
}
# kill 掉项目
function f_kill() {
ssh $TARGET_HOST_NAME "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"
}
# run 项目
function f_run() {
ssh $TARGET_HOST_NAME "cd ${APP_DIR}; source /etc/profile; nohup java -jar ${APP_JAR} --eureka.instance.ip-address=${IP_ADDRESS} --spring.profiles.active=prod ${LAUNCH_PARAMS} > /dev/null 2>&1 &"
}
# 匹配 IP 地址
function f_matchIp() {
IP_ADDRESS=$(grep ${TARGET_HOST_NAME} ./ip | awk '{printf $2}')
}
echo -e "\n==================== 微服务统一部署脚本 ${SH_VERSION} ==================== \n"
#cat ./projectData | grep -v '^#' | awk -F '\t' '{print NR ". " $1}'
# 一行两列格式化对齐输出
cat ./projectData | grep -v '^#' | awk -F '\t' 'NR%2==1 {printf ("%s. %-20s \t\t",NR,$1)} NR%2==0 {print NR ". " $1}'
echo -e "\n"
read -p "请选择项目编号(输入 0 退出):" projectDataN
if [ "$projectDataN" == 0 ]; then
exit 1
fi
uiName=''
BASE_DIR=''
PROJECT=''
APP_DIR=''
PUSH_TARGET_HOST_NAMES=''
LAUNCH_PARAMS='' # 额外启动参数
# 获取到该项目一行的配置数据
eval $(cat ./projectData | grep -v '^#' | sed -n "${projectDataN}p" | awk -F '\t' '{ printf("uiName=%s;BASE_DIR=%s;PROJECT=%s;APP_DIR=%s;PUSH_TARGET_HOST_NAMES=%s;LAUNCH_PARAMS=%s",$1,$2,$3,$4,$5,$6)}')
echo -e "\n当前操作项目为:${uiName}\n"
echo "1. 打包并推送项目"
echo "2. kill 项目"
echo "3. run 项目"
echo "4. kill & run 项目"
echo "5. 检测项目是否运行"
echo -e "\n"
read -p "请选择:" optNu
APP_JAR="${PROJECT}-boot.jar"
TARGET_HOST_NAME=${PUSH_TARGET_HOST_NAMES}
IP_ADDRESS='' # ip 地址
case $optNu in
1)
echo "已选择:打包并推送项目"
f_pack
;;
2)
echo "已选择:kill 项目"
f_checkSelectHostName
f_matchIp
f_kill
;;
3)
echo "已选择:run 项目"
f_checkSelectHostName
f_matchIp
f_run
;;
4)
echo "已选择:kill & run 项目"
f_checkSelectHostName
f_matchIp
f_kill
f_run
;;
5)
echo "已选择:检测项目是否运行"
f_checkSelectHostName
f_matchIp
f_checkIsRun $APP_JAR
;;
*)
echo "选择错误,已退出"
esac
# function_print $uiName $BASE_DIR $PROJECT $APP_DIR $PUSH_TARGET_HOST_NAMES
projectData
微服务信息文件
# ui界面显示名称 BASE_DIR源(码目录) PROJECT(项目名称) APP_DIR(部署目录) PUSH_TARGET_HOST_NAMES(要部署的机器名称,多个用逗号隔开) LAUNCH_PARAMS(其他启动参数)
# 建议多实例相同启动参数,直接使用多 PUSH_TARGET_HOST_NAMES ,而不同启动参数,写多行
"mrcode-cloud-sam" "/data/packaging/git-code" "mrcode-cloud-sam" "/mnt/service/$PROJECT" "cloud-05" ""
"mrcode-cloud-sam-agent-1" "/data/packaging/git-code" "mrcode-cloud-sam-agent" "/mnt/service/$PROJECT" "cloud-05" "--spring.cloud.stream.instance-index=0"
"mrcode-cloud-sam-agent-2" "/data/packaging/git-code" "mrcode-cloud-sam-agent" "/mnt/service/$PROJECT" "cloud-04" "--spring.cloud.stream.instance-index=1"
"mrcode-cloud-sem" "/data/packaging/git-code" "mrcode-cloud-sem" "/mnt/service/$PROJECT" "cloud-04" ""
"mrcode-cloud-sem-google" "/data/packaging/git-code" "mrcode-cloud-sem-google" "/mnt/service/$PROJECT" "cloud-05" ""
"mrcode-cloud-sem-facebook" "/data/packaging/git-code" "mrcode-cloud-sem-facebook" "/mnt/service/$PROJECT" "cloud-05" ""
"mrcode-cloud-product" "/data/packaging/git-code" "mrcode-cloud-product" "/mnt/service/$PROJECT" "cloud-05" ""
"mrcode-cloud-feed" "/data/packaging/git-code" "mrcode-cloud-feed" "/mnt/service/$PROJECT" "cloud-05" ""
"mrcode-cloud-site-group" "/data/packaging/git-code" "mrcode-cloud-site-group" "/mnt/service/$PROJECT" "cloud-04" ""
"mrcode-cloud-life-cycle" "/data/packaging/git-code" "mrcode-cloud-life-cycle" "/mnt/service/$PROJECT" "cloud-04 cloud-05" ""
"mrcode-cloud-notice" "/data/packaging/git-code" "mrcode-cloud-notice" "/mnt/service/$PROJECT" "cloud-05" ""
"mrcode-cloud-gateway" "/data/packaging/git-code" "mrcode-cloud-gateway" "/mnt/service/$PROJECT" "cloud-06" ""
ip
host 与 ip 映射, 这个其实不太需要,主要是为了传递内网 IP 给 --eureka.instance.ip-address
参数
cloud-02 192.168.7.70
cloud-03 192.168.7.71
cloud-04 192.168.7.72
cloud-05 192.168.7.2
cloud-06 192.168.7.73
cloud-07 192.168.7.74