驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
基于阿里云自动化部署Flow的记录
/  

基于阿里云自动化部署Flow的记录

开篇

当前负责项目前期只有几台服务器,一段时间是人肉运维,线下编码、测试、打包后通过工具打包到到服务,然后通过编写好的脚本进行启动和关闭。这种人肉运维的方式直到直到服务器数量扩充后,就显得低效而重复了。

这个时候刚好了解到阿里云提供的Devops有一套流水线Flow,其中有一个功能能解决自动化部署的问题。抽了时间从头了解了下,发现挺好用的,遂将线上版本发布改为了Flow,这里记录下中间过程和踩得坑。

正文

阿里云的流水线可以支持很多功能,包括:线上版本发布、自动化测试、安全信息扫描等等,这里我主要是记录下线上版本发布。

在 流水线定义中,选择Java · 构建、部署到阿里云ECS/自有主机这个模板,会出现3个步骤:

  • 源:选择对应分支的代码进行编译
  • 构建:选择代码的构建方式,比如 Maven/Gradle 等
  • 部署:通过构建好的项目和发布脚本在线下发布

就是选择线上发布的是哪一个分支。这个不需要展开说。

构建

这个阶段就是选择编译方式进行打包并将生成的包上传到存储空间。

第一步就是将对应的代码通过指定的方式进行编译打包,因为我们项目采用的是Maven,所以接下来通过Maven描述。构建时候需要选择:JDK版本、Maven版本、构建命令。根据自己项目实际需求进行选择即可。

第二步就是需要特别说下的构建物上传这个细节。构建物通常除了生成的target而外,还需要一个部署脚本,通常命名为depoloy.sh。它位于项目的根据目录下,所以在上传构建物的时候,一定要手动添加上这个deploy.sh.

image.png

构建物上传本质上不是上传到你的服务器,而是上传到一个存储空间,需要在对应的服务器上传,不过这个步骤流水线替我们做了。

主机部署

该阶段需要做的事情就是将上述步骤的构建的包现在到服务器,然后通过部署脚本进行执行。以下罗列关键步骤

主机组:其意思就是你应用执行的服务器。需要选定某些服务器作为你的发布组,这些主机可以是阿里云下的绑定资源,也可以通过对应的指令自行安装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编码
  • 启动脚本的编写和修改

希望本文能给大家一些收获。

不积跬步,无以至千里。不积小流,无以成江海。