当前负责项目前期只有几台服务器,一段时间是人肉运维,线下编码、测试、打包后通过工具打包到到服务,然后通过编写好的脚本进行启动和关闭。这种人肉运维的方式直到直到服务器数量扩充后,就显得低效而重复了。
这个时候刚好了解到阿里云提供的Devops
有一套流水线Flow
,其中有一个功能能解决自动化部署的问题。抽了时间从头了解了下,发现挺好用的,遂将线上版本发布改为了Flow
,这里记录下中间过程和踩得坑。
阿里云的流水线可以支持很多功能,包括:线上版本发布、自动化测试、安全信息扫描等等,这里我主要是记录下线上版本发布。
在 流水线定义中,选择Java · 构建、部署到阿里云ECS/自有主机
这个模板,会出现3个步骤:
就是选择线上发布的是哪一个分支。这个不需要展开说。
这个阶段就是选择编译方式进行打包并将生成的包上传到存储空间。
第一步就是将对应的代码通过指定的方式进行编译打包,因为我们项目采用的是Maven
,所以接下来通过Maven
描述。构建时候需要选择:JDK
版本、Maven
版本、构建命令。根据自己项目实际需求进行选择即可。
第二步就是需要特别说下的构建物上传
这个细节。构建物通常除了生成的target
而外,还需要一个部署脚本,通常命名为depoloy.sh
。它位于项目的根据目录下,所以在上传构建物的时候,一定要手动添加上这个deploy.sh
.
构建物上传本质上不是上传到你的服务器,而是上传到一个存储空间,需要在对应的服务器上传,不过这个步骤流水线替我们做了。
该阶段需要做的事情就是将上述步骤的构建的包现在到服务器,然后通过部署脚本
进行执行。以下罗列关键步骤
主机组:其意思就是你应用执行的服务器。需要选定某些服务器作为你的发布组,这些主机可以是阿里云下的绑定资源,也可以通过对应的指令自行安装agent
后成为。
部署配置:关键是设定好对应的下载路径。“下载路径”指的就是第二阶段构建好的构建物,Flow
会将其打包后上传到对应的存储空间,这个阶段Flow
会自动下载到设定的下载路径
部署脚本:就是将“下载路径”中的构建物解压并通过命令执行的过程。
上述的流程不难,走一遍基本上七七八八了,接下来的内容我认为才是重点。
为了灵活性和安全性,项目启动脚本deploy.sh
中会涉及到一些启动参数,不是写死的,而是从启动时候的环境变量中读取的,比如某些密码、模板变量等。
对于某些密码难免有一些特殊字符,但是Flow
流水线虽然支持设置流水线的变量,但是变量内容是不支持特殊字符的,此时我认为最好的方式就是:将包含特殊字符的变量进行Base64
编码,然后在deploy.sh
脚本进行Base64
反编码。
对于变量的命名也建议规范,切记不要包含特殊字符。比如user.pwd
就是严重错误的,建议修改为user_pwd
这种。
整个流水线可能会涉及到 2 个脚本。
deploy.sh
deploy.sh
:封装了项目启动、关闭、重启、健康监测等信息这里我给出我们项目目前使用的部署脚本和项目启动脚本。
部署脚本
# 新建项目执行目录
mkdir -p /home/admin/${APP_FULL_NAME}
# 解压构建物压缩包 到 执行目录
tar -zxf /home/admin/app/ubirth-package.tgz -C /home/admin/${APP_FULL_NAME}
# 执行启动脚本
sh /home/admin/${APP_FULL_NAME}/deploy.sh restart
启动脚本
#!/bin/bash
# 全局变量有特殊编码需要先进行 Base64加密,再在此处解密
export SPRING_BOOT_ADMIN_CLIENT_PASSWORD=$(echo $SPRING_BOOT_ADMIN_CLIENT_PASSWORD | base64 -d)
export PLUME_REDIS_PWD=$(echo $PLUME_REDIS_PWD | base64 -d)
# 修改APP_FULL_NAME为云效上的应用名
APP_NAME=ubirth-server
APP_VERSION=4.0.2012
APP_FULL_NAME=${APP_NAME}-${APP_VERSION}
PROG_NAME=$0
ACTION=$1
APP_START_TIMEOUT=120 # 等待应用启动的时间
APP_PORT=12000 # 应用端口
JMX_PORT=2021 #JMX 端口
DEBUG_PORT=2022 #远程调试端口
HEALTH_CHECK_URL=http://127.0.0.1:${APP_PORT} # 应用健康检查URL
APP_HOME=/home/admin/${APP_FULL_NAME} # 从package.tgz中解压出来的jar包放到这个目录下
JAR_NAME=${APP_HOME}/target/${APP_FULL_NAME}.jar # jar包的名字
JAVA_OUT=${APP_HOME}/logs/start.log #应用的启动日志
# 创建出相关目录
mkdir -p ${APP_HOME}
mkdir -p ${APP_HOME}/logs
mkdir -p ${APP_HOME}/gc
usage() {
echo "Usage: $PROG_NAME {start|stop|restart}"
exit 2
}
health_check() {
exptime=0
echo "checking ${HEALTH_CHECK_URL}"
while true; do
status_code=$(/usr/bin/curl -L -o /dev/null --connect-timeout 5 -s -w %{http_code} ${HEALTH_CHECK_URL})
if [ "$?" != "0" ]; then
echo -n -e "\rapplication not started"
else
echo "code is $status_code"
if [ "$status_code" == "200" ]; then
break
fi
fi
sleep 1
((exptime++))
echo -e "\rWait app to pass health check: $exptime..."
if [ $exptime -gt ${APP_START_TIMEOUT} ]; then
echo 'app start failed'
exit 1
fi
done
echo "check ${HEALTH_CHECK_URL} success"
}
start_application() {
# JAVA 启动参数
JAVA_OPT="${JAVA_OPT} -Xmx2048M -Xms2048M -Xmn768M -XX:MaxMetaspaceSize=412M -XX:MetaspaceSize=412M"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
JAVA_OPT="${JAVA_OPT} -XX:+ParallelRefProcEnabled -XX:+PrintCommandLineFlags"
JAVA_OPT="${JAVA_OPT} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/gc"
JAVA_OPT="${JAVA_OPT} -XX:ErrorFile=${APP_HOME}/gc/hs_err_pid%p.log"
JAVA_OPT="${JAVA_OPT} -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.port=${JMX_PORT}"
JAVA_OPT="${JAVA_OPT} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${DEBUG_PORT}"
JAVA_OPT="${JAVA_OPT} -Dspring.profiles.active=prod"
JAVA_OPT="${JAVA_OPT} -Dfastjson.parser.safeMode=true"
JAVA_OPT="${JAVA_OPT} -Dskywalking.instance.name=$(hostname)" #读取主机名称
JAVA_OPT="${JAVA_OPT} -Dplume.redis.host=${PLUME_REDIS_HOST}" #环境变量获取 Redis 的主机
JAVA_OPT="${JAVA_OPT} -Dplume.redis.pwd=${PLUME_REDIS_PWD}" #环境变量获取 Redis 的密码
JAVA_OPT="${JAVA_OPT} -Dplume.redis.database=${PLUME_REDIS_DATABASE}" #环境变量获取 Redis 的仓库
JAVA_OPT="${JAVA_OPT} -Dspring.boot.admin.client.username=${SPRING_BOOT_ADMIN_CLIENT_USERNAME}" #环境变量获取 SpringBoot Admin 的用户名
JAVA_OPT="${JAVA_OPT} -Dspring.boot.admin.client.password=${SPRING_BOOT_ADMIN_CLIENT_PASSWORD}" #环境变量获取 SpringBoot Admin 的密码
JAVA_OPT="${JAVA_OPT} -javaagent:/home/admin/skywalking/agent-8.0/skywalking-agent.jar" #skywalking 的代理路径
echo "starting java process"
#nohup java -jar ${JAR_NAME} > ${JAVA_OUT} 2>&1 &
nohup java ${JAVA_OPT} -jar ${JAR_NAME} >/dev/null 2>&1 &
echo "started java process"
}
stop_application() {
# 根据名称进行查找
# pid=`ps -ef | grep java | grep ${APP_FULL_NAME} | grep -v grep |grep -v 'deploy-back.sh'| awk '{print$2}'`
# 通过端口查找
pid=$(netstat -nlp | grep :${APP_PORT} | awk '{print $7}' | awk -F"/" '{ print $1 }')
if [[ ! $pid ]]; then
echo -e "\rno java process"
return
fi
echo "stop java process"
times=60
for e in $(seq 60); do
sleep 1
COSTTIME=$(($times - $e))
#pid=$(ps -ef | grep java | grep ${APP_FULL_NAME} | grep -v grep |grep -v 'deploy-back.sh'| awk '{print$2}')
pid=$(netstat -nlp | grep :${APP_PORT} | awk '{print $7}' | awk -F"/" '{ print $1 }')
if [[ $pid ]]; then
kill -9 $pid
echo -e "\r -- stopping java lasts $(expr $COSTTIME) seconds."
else
echo -e "\rjava process has exited"
break
fi
done
echo ""
}
start() {
start_application
health_check
}
stop() {
stop_application
}
case "$ACTION" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
usage
;;
esac
上述的脚本是我通过官方示例脚本进行修改的,主要修改了如下几个细节
Base64
的反解密端口
进行进行号获取这个过程中,更深入的了解到了Devops
的自动化部署,感觉比Jekins
更简单些。同时踩了几个坑:
deploy.sh
Base64
编码希望本文能给大家一些收获。