前言
Github Actions是一个持续集成和持续交付 (CI/CD) 平台,可用于自动执行构建、测试和部署管道。 您可以创建工作流程来构建和测试存储库的每个拉取请求,或将合并的拉取请求部署到生产环境。
Github Actions文档
最近写代码的时候看见项目配置了流水线,所以就想着给自己的项目也搞个自动化部署节省时间,于是就研究了一下Github Actions,一研究就感觉这东西也太强大了吧,不光是能实现自动部署,还能实现定时任务一类的功能,最重要的是个人使用的话用免费的套餐就完全够用了,突然感觉Github Action就跟做慈善似的😂。
收费
公共仓库和自托管运行器免费使用 GitHub Actions。 对于私有仓库,每个 GitHub 帐户可获得一定数量的免费记录和存储,具体取决于帐户所使用的产品。 超出包含金额的任何使用量都由支出限制控制。
关于 GitHub Actions 的计费
Github Actions是按照流水线的运行时间计费的,免费套餐的使用限制是每个月2000分钟(Linux环境下),而在Windows 和 macOS 运行器上运行的作业,其消耗分钟数是在 Linux 上运行的作业的 2 倍和 10 倍。账户的使用量可以在Settings,Billings and plans里面查看。
目标
那么我们的任务(Job)就是实现SpringBoot的自动构建和部署至服务器上了,如果你的项目并不是基于SpringBoot,也没关系,已经有很多别人写好的脚本,总有一个是能编译你的项目的,而我们只需要简单的配置就能使用了。那么我们在哪里找别人写好的脚本呢,答案是Github Action Marketplace,整个配置过程其实就是调用别人写好的脚本来完成我们希望的工作流程(Wockflow),所以我们首先要把任务拆解成一个一个的步骤(Step)。对于SpringBoot写的项目,要更新服务器上的文件,我们需要这几个步骤:
- 打包
- 上传至服务器
- 重启服务
所以我们的任务需要做的就是依次执行好上面的三个步骤就可以了。
步骤
寻找Actions
刚刚说了,我们要做的其实就是用别人造好的轮子(如果你想自己造的话也可以),那么我们首先就要在Github Action Marketplace上找到我们需要的轮子。下面是我用到的轮子(官方的轮子就省略了):
第一个的作用是把编译好的文件上传到服务器上,第二个的作用是在服务器上运行命令也就是拿来重启服务;他们的详细介绍和文档可以在他们的Github页上看到。
编写Actions
打开项目的仓库,在顶部找到Actions,Java with Maven,Configure
点击后我们就进入了编辑界面
name: Java CI with Maven
on: # 触发器
push:
branches: [ "main" ] # main分支有push请求时触发
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest # 工作的运行环境
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11' # 设置Java的版本
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
简单介绍一下上面的意思,
第一行的name
就是这个工作流程的名字,后面的on
是这个工作流程的触发器,push
就是发生push请求的时候,pull_request
是合并分支的时候,branches: [ "main" ]
当然就是指定为main分支了。
jobs
中的build
是当前作业的名称,runs-on
就是这个作业的运行环境,这里就是最新的ubuntu系统;
steps
就是这个作业所有的步骤,每个步骤用-
来区分开,uses
就是使用别人写好的轮子了;name
是这个步骤的名称,之后这个名称会在查看Actions执行状态的时候显示出来;java-version
就是指定java的版本,有哪些选择可以在actions/setup-java里面查看。
run
就是执行命令了。
理解大致的意思之后就能写了,下面是我的示例(事先服务化部署了项目,直接通过Systemctl重启服务就可以了)
name: Build And Deploy CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@main
- name: Set up JDK 1.8
uses: actions/setup-java@v3
with:
java-version: '8'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
- name: Rename target
run: |
mv target/*.jar target/example.jar # 重命名编译出来的文件
- name: Deploy to server
uses: easingthemes/ssh-deploy@v2.2.11
env:
ARGS: '-avz --delete'
SOURCE: 'target/example.jar'
TARGET: '/var/www/example/' # 把example.jar上传至/var/www/example目录
REMOTE_HOST: ${{ secrets.SERVER_HOST }}
REMOTE_USER: ${{ secrets.SERVER_USER }}
SSH_PRIVATE_KEY: ${{ secrets.SERVER_ACCESS_KEY }} # 登录服务器的私钥
- name: Restart service
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.SERVER_ACCESS_KEY }}
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
script_stop: true
timeout: 15s
script: | # 通过systemctl重启服务
echo ${{ secrets.SERVER_SUDO_PASS }} | sudo -S systemctl restart example
注意到上面的文件里面出现了很多${{ secrets.XXXX }}
格式的字段,这个是Actions secrets,因为Action的配置是以文件的形式存储在仓库里面的,拿来放敏感信息就非常不安全,所以我们可以设置成Actions secrets,再通过变量访问,同时这个secrets是只有项目的协作者才能修改的。
要配置Actions secrets,需要打开项目仓库,在Settings,Serects,Actions中添加或修改
同时注意,Actions secrets是针对某一个项目的,目前还没有全局的secrets。
appleboy/ssh-action
时,他的host
是支持有多个的,也就是说如果你需要在多台服务器上执行相同的代码,那么这个功能将非常方便(但是这几台服务器的登录验证必须一致)。示例:
host: '${{ secrets.SERVER_ONE_HOST }},${{ secrets.SERVER_TWO_HOST }}'
编辑好之后,就能保存提交了,这个提交之后就会立刻触发工作流程,我们可以在Action中查看当前工作流程的状态。如果出错了,也能在控制台里面看到错误的详细信息,非常方便。
Docker上的服务更新
上面的方法是针对于软件直接裸机运行在服务器上的,对于Docker部署的项目来说,之前的打包上传都差别不大,主要的难点是服务重启,这里有两种方法来实现。(下面的示例都以SpringBoot项目为例)
Github Actions构建Docker镜像
这个方法的主要思路是通过Github Actions来构建Docker镜像,然后打包上传到服务器,再在服务器上载入并启用。同时因为生产环境的配置信息(数据库密码啥的)不能泄露,所以配置文件不能上传到Git,这里就需要直接在服务器上挂载外部的配置文件。
既然要创建镜像了,那么肯定就需要Dockerfile文件了,在项目根目录创建Dockerfile文件,并写入下面的内容
FROM eclipse-temurin:8-jre # 基础镜像
MAINTAINER NuoTian # 作者
VOLUME /tmp # 指定临时文件夹
ADD target/example.jar . # 把maven打包好的文件导入进去
RUN bash -c 'touch /example.jar'
ENTRYPOINT ["java","-jar","/example.jar","--spring.profiles.active=prod"]
EXPOSE 8080 # 暴漏外部端口
Dockerfile文件将在Github Actions运行的时候使用并构建一个镜像,接下来的操作就是打包上传重启服务了。
name: Build And Deploy CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@main
- name: Set up JDK 1.8
uses: actions/setup-java@v3
with:
java-version: '8'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
- name: Rename target
run: |
mv target/*.jar target/example.jar
- name: Set up Docker
uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: false
- name: Build image
run: |
docker build -t nuotian/example:latest .
- name: Save image
run: |
docker save -o example.tar nuotian/example:latest
- name: Upload to server
uses: easingthemes/ssh-deploy@v2.2.11
env:
ARGS: '-avz --delete'
SOURCE: 'target/example.tar'
TARGET: '/var/www/example/'
REMOTE_HOST: ${{ secrets.SERVER_HOST }}
REMOTE_USER: ${{ secrets.SERVER_USER }}
SSH_PRIVATE_KEY: ${{ secrets.SERVER_ACCESS_KEY }}
- name: Update and restart Docker
uses: appleboy/ssh-action@v0.1.4
with:
key: ${{ secrets.SERVER_ACCESS_KEY }}
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
script_stop: true
timeout: 15s
script: |
echo ${{ secrets.SERVER_SUDO_PASS }} | sudo -S docker rm -f $(echo ${{ secrets.SERVER_SUDO_PASS }} | sudo -S docker ps -a | grep nuotian/example:latest | awk '{print $1}')
echo ${{ secrets.SERVER_SUDO_PASS }} | sudo -S docker load < /var/www/example/example.tar
echo ${{ secrets.SERVER_SUDO_PASS }} | sudo -S docker run -d -p 8081:8080 -v /var/www/example/config/application-prod.yml:/config/application-prod.yml nuotian/example:latest
上面很多的内容都跟前面小节讲的一样,这里就不过多说明了,主要是最后的三行命令比较重要,依次解释一下。
第一行,docker rm -f $(docker ps -a | grep nuotian/example:latest | awk '{print $1}')
这里就是删除以前的容器,需要注意的是如果服务器上没有镜像为nuotian/example:latest
的容器的话这段会报错,所以运行前请先在服务器上运行一个容器(docker run -d -p 8080:8080 nuotian/example:latest
)。
第二行,docker load < /var/www/example/example.tar
这里就是导入包了,没什么好说的。
第三行,docker run -d -p 8081:8080 -v /var/www/example/config/application-prod.yml:/config/application-prod.yml nuotian/example:latest
就是创建一个容器,并把容器的8080端口映射到本地的8081端口,然后把主机里面的/var/www/example/config/application-prod.yml
文件映射至/config/application-prod.yml
,这样软件启动的时候就会读取到配置文件了。
镜像运行外部Jar文件(推荐)
这个方法的思路是不把Jar文件打包至镜像内,通过Docker挂载文件的功能把主机里面的Jar文件挂载到容器内,然后容器内运行java -jar
命令。这个方法有几个好处,首先是避免了Github Actions要构建镜像文件而导致运行时间很长;其次是要上传到服务器的是Jar文件,它比Docker导出的tar文件要小很多,这也是能缩短流水线运行的时间。
同样,这个方法的配置文件也是通过挂载的方式让容器读取的。
这个方法因为需要事先在服务器上运行好Docker,所以我们首先要把打包好的jar和Dockerfile文件(只需要放在服务器内就行,不用上传到Github;刚刚那一小节里面的示例就行)上传到服务器上的同一个文件夹内,然后执行命令
sudo docker build -t nuotian/example:latest .
构建好镜像之后再运行
sudo docker run -d -p 8081:8080 -v /var/www/example/example.jar:/example.jar -v /var/www/example/config/application-prod.yml:/config/application-prod.yml --name ExampleContainer eclipse-temurin:17-jre java -jar /example.jar --spring.profiles.active=prod
确认容器已经在运行了之后,就能写Github Actions的配置文件了
name: Build And Deploy CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@main
- name: Set up JDK 1.8
uses: actions/setup-java@v3
with:
java-version: '8'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
- name: Rename target
run: |
mv target/*.jar target/example.jar
- name: Deploy to server
uses: easingthemes/ssh-deploy@v2.2.11
env:
ARGS: '-avz --delete'
SOURCE: 'target/example.jar'
TARGET: '/var/www/example/'
REMOTE_HOST: ${{ secrets.SERVER_HOST }}
REMOTE_USER: ${{ secrets.SERVER_USER }}
SSH_PRIVATE_KEY: ${{ secrets.SERVER_ACCESS_KEY }}
- name: Restart service
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.SERVER_ACCESS_KEY }}
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
script_stop: true
timeout: 15s
script: | # 重启名叫ExampleContainer的容器
echo ${{ secrets.SERVER_SUDO_PASS }} | sudo -S docker restart ExampleContainer
这个配置跟最开始给出的示例只有最后重启服务的命令有所不同,总的来说这个方法更好一些。到现在就完成了,可以commit然后查看效果了。
SpringBoot项目在Docker中运行存在的坑
如果你发现自己的项目查看日志能正常运行,但是怎么也访问不了,用curl localhost:8080
测试端口提示Connection reset by peer
,可以看看SpringBoot的配置文件中的server.address
如上图中的127.0.0.1就是问题所在,把它改成0.0.0.0或者直接删了就好了(直接删除可能也会出现问题)。