驽马十驾 驽马十驾

驽马十驾,功在不舍

目录
基于阿里云自动化部署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编码
  • 启动脚本的编写和修改

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

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