一、概述
在项目数量比较大和构建流程比较复杂的场景,我们一般会使用jenkins以及衍生产品来实现构建打包部署能力,但对于一些简单的项目和小众场景,我们使用简单的脚本构建部署,也未必不是一个简单便捷和节省成本的选择。
我们以jenkins为例,其本质就是从远程仓库拉取代码,然后本地编译打包,然后上传到目标服务器执行启动命令,简化过程如下:
那么在一些简单的项目中,我们可不可以完全自己写一个脚本来做打包部署呢,答案是可以的,我们可以模仿jenkins的工作流程并且做一些简化:
- 从git拉取项目代码到服务器
- 使用maven命令进行编译打包,打成可执行的jar
- 使用命令或者其他工具启动java服务(java -jar,docker等等)
这样原本在jenkins执行的工作,转移到了服务器本机执行了。
二、编写部署脚本
前边有介绍到通过脚本来部署应用程序,那么就需要目标服务器拥有执行相关拉取代码、编译、构建的能力,比如最基本的java运行环境、maven工具、git命令等,如果是借助docker启动服务,那么还需要安装docker相关工具。
1.环境准备
java
yum install -y java-1.8.0-openjdk-devel
#/etc/profile环境变量
# jdk 8
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.382.b05-1.amzn2.0.2.x86_64
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin
# 保存后,source生效
source /etc/profile
maven
mkdir /opt/tools/maven
cd /opt/tools/maven
wget https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.8.8/binaries/apache-maven-3.8.8-bin.tar.gz
tar -zxvf apache-maven-3.8.8-bin.tar.gz
#配置环境变量
vim /etc/profile
# maven 3.8.8
export MAVEN_HOME=/opt/tools/maven/apache-maven-3.8.8
export PATH=${PATH}:${MAVEN_HOME}/bin
#保存后source生效
source /etc/profile
git
yum -y install git
配置访问公钥,在服务器上生成公钥:
ssh-keygen
然后把~/.ssh/id_rsa.pub内容添加到远程仓库的ssh秘钥中:
这样服务器就可以通过git命令从远程仓库拉取代码了。
2.基于java命令启动的部署脚本
我们以项目springboot-demo为例,创建项目路径:
mkdir -p /opt/app/server/springboot-demo
编写部署脚本:
cd /opt/app/server/springboot-demo
touch start.sh
chmod +x start.sh
start.sh脚本内容如下:
#!/bin/bash
#项目路径
WORK_DIR=/opt/app/server/springboot-demo
#项目名称
PROJECT_NAME=springboot-demo
#获取代码
cd $WORK_DIR
if [ ! -d $PROJECT_NAME ];then
#如果项目文件夹内不存在,则从远程仓库拉取指定分支代码
git clone -b branch_name git@gitlab.com:xxx.git
#进入应用目录
cd $PROJECT_NAME
else
#如果项目文件夹存在,说明之前拉取过,那么进入项目路径拉取最新代码
cd $PROJECT_NAME
git pull
fi
#maven构建
mvn -U clean compile package -Dmaven.test.skip=true -P$1
# 如果构建失败,退出脚本
if [ $? -ne 0 ]; then
echo "maven build failue!"
exit 1
fi
#部署项目
#找到之前启动的服务进程
SPRINGBOOT_DEMO_PID=$(ps -ef | grep "springboot-demo-$1.jar" | egrep -v "grep|$$" | awk 'NR==1{print $2}')
#如果已经存在进程,则发送kill信号终止
[ -n "$SPRINGBOOT_DEMO_PID" ] && kill $SPRINGBOOT_DEMO_PID
# 休眠10s,等待进程终止
sleep 10
#把maven编译打包的最新jar包拷贝到工作目录
cp target/springboot-demo-$1.jar $WORK_DIR
#再次检查进程是否终止,如果没有终止则发送kill -9信号强行终止
[ -n "$SPRINGBOOT_DEMO_PID" ] && kill -9 $SPRINGBOOT_DEMO_PID
#使用nohup java -jar命令后台启动服务
nohup java -Djava.security.egd=file:/dev/./urandom -Xms1g -Xmx1g -Xmn512m -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+CMSScavengeBeforeRemark -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:/mnt/applogs/$springboot-demo/gc.log -Dfile.encoding=utf-8 -jar $WORK_DIR/springboot-demo-$1.jar >/dev/null 2>&1 &
echo "springboot-demo startup success"
该脚本核心做了以下几件事情:
- 从远程仓库拉取项目代码;如果已经存在项目目录,则进入目录拉取最新代码
- 使用mvn命令编译打包,并输出可执行jar到target目录,如果编译失败则退出执行
- 找出服务进程,并发送kill执行进行终止服务进程,并且休眠10s,给服务进程足够的时间处理剩余的事情
- 从项目目录的target文件夹拷贝可执行jar到项目工作目录
- 再次检查服务进程是否已经终止,如果没有终止则强行终止(理论上10s可以正常终止,休眠时间可按需调整)
- 通过nohup java -jar命令后台运行服务,启动成功后打印启动成功日志
执行start.sh脚本打包部署:
sh start.sh dev
通过脚本的输入日志可以看到服务已经打包部署成功了:
使用netstat命令检查端口已经监听成功,并且发送请求也能够正常处理:
这样我们通过脚本来实现java服务的代码拉取、编译打包和服务启动已经成功了。
3.基于docker启动的部署脚本
有些项目团队喜欢使用docker启动java服务,那么我们同样可以将上述脚本稍做改造,来实现基于shell+docker的简单项目部署能力。
安装docker运行环境(服务器是aws ec2):
sudo yum update -y
sudo amazon-linux-extras install docker
sudo service docker start
sudo systemctl enable docker
项目路径不再重复创建,还是基于上一小节的路径,在项目工作路径创建Dockerfile文件,内容如下:
FROM openjdk:8
ARG PROFILES
ARG APP_NAME_ARG
ARG SERVER_PORT_ARG
ENV PROFILES_ACTIVE ${PROFILES}
ENV APP_SERVER_PORT ${SERVER_PORT_ARG}
ENV LANG=zh_CN.UTF-8
RUN apt-get update && apt-get install -y locales && sed -i 's/# zh_CN.UTF-8 UTF-8/zh_CN.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
VOLUME /tmp
ADD ${APP_NAME_ARG}-${PROFILES}.jar /app.jar
RUN bash -c 'touch /app.jar'
EXPOSE ${APP_SERVER_PORT}
ENTRYPOINT ["sh", "-c", " date && java -XX:+UseG1GC -Dfile.encoding=utf-8 -Dserver.port=${APP_SERVER_PORT} \
-jar /app.jar --spring.profiles.active=${PROFILES_ACTIVE}"]
Dockerfile文件接收三个参数:
- PROFILES:激活的profile
- APP_NAME_ARG:应用名称
- SERVER_PORT_ARG:服务端口
同样在项目工作目录创建start.sh脚本:
cd /opt/app/server/springboot-demo
touch start.sh
chmod +x start.sh
脚本内容如下:
#!/bin/bash
WORK_DIR=/opt/app/server/springboot-demo/
PROJECT_NAME=springboot-demo
#获取代码
cd $WORK_DIR
if [ ! -d $PROJECT_NAME ];then
git clone -b branch_name git@gitlab.com:xxx.git
cd $PROJECT_NAME
else
cd $PROJECT_NAME
git pull
fi
#maven构建
mvn -U clean compile package -Dmaven.test.skip=true -P$1
if [ $? -ne 0 ]; then
echo "maven build failue!"
exit 1
fi
#部署项目
cp target/springboot-demo-$1.jar $WORK_DIR
docker build --build-arg PROFILES=$1 --build-arg APP_NAME_ARG=$2 --build-arg SERVER_PORT_ARG=$3 -t $2:0.0.1 .
docker stop $2
docker rm $2
docker rmi $(docker images | awk '/^<none>/ { print $3 }')
docker run -v /mnt:/mnt -p $3:$3 --restart=always --name $2 -d $2:0.0.1
echo "springboot-demo startup success"
该脚本和前边的类似,做了以下几件事情:
- 从远程仓库拉取项目代码;如果已经存在项目目录,则进入目录拉取最新代码
- 使用mvn命令编译打包,并输出可执行jar到target目录,如果编译失败则退出执行
- 从项目目录的target文件夹拷贝可执行jar到项目工作目录
- 使用docker命令构建java服务镜像,并定义传入三个入参
- 停止老的docker中的java服务容器,并移除
- 找到老的java服务镜像,并移除
- 启动新的java服务容器,启动成功后打印启动成功日志
执行start.sh脚本打包部署:
sh start.sh dev springboot-demo 8099
从构建日志可以看到脚本已经执行成功:
使用docker images看到镜像已经构建:
使用docker ps可以看到java服务容器已经启动,并且容器内端口已经和宿主机的端口绑定映射成功:
使用netstat命令检查端口已经监听成功,并且发送请求也能够正常处理:
这样我们通过改造部署脚本也实现了java服务的代码拉取、编译打包和docker容器启动。
三、扩展
考虑到jenkins以及衍生产品处理提供完善的流程化的部署能力,也会提供详细的部署记录以及通知能力,我们也可以将脚本进行改造,来提供相应的能力,比如记录什么时间出发了打包部署,部署成功和失败的通知等。
1.部署日志记录
将部署开始和结束的日志追加到部署日志中。
# 日志文件路径
LOG_FILE="/opt/app/server/springboot-demo/deploy.log"
# 记录当前时间和执行的命令到日志文件
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting start.sh script" >> "$LOG_FILE"
# 部署脚本内容
# 记录脚本执行结束的时间到日志文件
echo "[$(date '+%Y-%m-%d %H:%M:%S')] End of start.sh script" >> "$LOG_FILE"
2.部署结果通知
可以将核心节点的错误或者失败内容通过webhook发送到对应的告警平台,比如钉钉、飞书机器人等。
以maven编译打包失败发送飞书告警为例:
ROBOT_TOKEN="xxxxxx"
REQ_PATH="https://open.feishu.cn/open-apis/bot/v2/hook/$ROBOT_TOKEN"
REQ_TYPE="Content-Type: application/json"
mvn -U clean compile package -Dmaven.test.skip=true -P$1
if [ $? -ne 0 ]; then
echo "maven build failue!"
BODY="{\"msg_type\":\"interactive\",\"card\":{\"config\":{\"wide_screen_mode\":false,\"enable_forward\":true},\"elements\":[{\"tag\":\"markdown\",\"content\":\" \n$CURR_DATE $CURR_TIME \n**服务器别名** : $CURR_IP \n\n\n**关注进程名** : $FOCUS_PROCESS \n**警告级别** : CRITICAL \n**警告内容** : mvn构建失败!\"}],\"header\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"[$CURR_ENV 环境]: server-Alert\"},\"template\":\"red\"}}}"
curl "$REQ_PATH" \
-H "$REQ_TYPE" \
-d "$BODY"
exit 1
fi
其他节点调整通知文案和内容即可。
四、总结
使用shell脚本来实现项目的打包部署比较轻量级,必要适合小团队和小众化项目的部署,相比于jenkins以及类似衍生产品打包部署有以下一些可能的优缺点:
优点:
- 定制化程度高: 通过编写自定义的 Shell 脚本,可以更灵活地满足特定项目的需求,定制化程度更高。
- 减少依赖:使用shell脚本可以减少对Jenkins的依赖,特别是在需要迁移或者更换持续集成工具时,减少了迁移的复杂性。
- 更轻量级:shell脚本相比jenkins Pipeline脚本或者其他持续集成工具的配置文件可能更加轻量级,易于维护和管理。
- 节省成本:jenkins部署项目时是比较吃服务器性能的,一般部署jenkins的服务器配置要比业务机器的配置高,使用脚本节省了部署jenkins的机器成本。
缺点:
- 可维护性较低:相比jenkins提供的可视化界面和各种插件,使用shell脚本可能会降低可维护性,尤其是对于不熟悉shell脚本的团队成员而言。
- 缺少监控和报告:jenkins 提供了丰富的监控和报告功能,如构建历史、构建日志、构建结果等,而使用 Shell 脚本可能需要自行实现这些功能。
- 学习成本:对于不熟悉shell脚本的团队成员来说,需要花费额外的时间和精力学习shell脚本语法和编写规范。
综上所述,使用shell脚本来替换jenkins打包部署具有一定的优势,但也需要考虑到一些潜在的缺点,并根据具体情况来权衡选择。