利用Github Action的CI/CD容器化部署思路
青 叶

之前是用的KNative Serverless,还算比较好用,可以使用kn cli直接处理镜像容器的问题,但是由于目前各大云服务器厂商的价格都比较高,我还是选择了海外的节点,海外的节点带宽较高,但是配置相对较低,KNative比较适合部署在高配机子上,所以只能探索一种新的CI/CD方案了。

利用Github Action的CI/CD容器化部署思路

Github Action

Github Action目前对普通用户也是免费使用的,貌似有一定的额度,但是对于个人用户而言肯定是足够的。

如果喜欢官方文档,可以去这里:https://docs.github.com/cn/actions

如果想精简一点,会用就行,可以接着看下面的内容,否则直接跳到第二部分即可。

快速开始Github Action

你需要在你的Git repository创建一个文件夹名为.github/workflows,这个文件夹名字必须是固定的。在其内部则可以创建你的工作流文件。

工作流文件是YAML格式的文件,例如官方给的Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

定制简单构建工作流

在开始前,必须明白CI中的一些术语:

  • workflow:持续集成的一次过程,即一个工作流
  • job:一个workflow包含若干个job,即工作流中的工作
  • step:一个job包含若干个step,每个step可以执行特定的操作,多个step组成一个完整的job
  • action:每个step可以依次执行多个命令(action)

接下来定制工作流,首先需要一个workflow模板,推荐如下:

1
2
3
4
5
6
7
8
name: Workflow Name

on:
push:
tags:
- "*-build"

jobs:

注意这里的name只是指定了工作流的名称,其中on是触发配置,如上所示,即会在repository的拥有者push且push的tag为xxxxx-build时才会触发此工作流。

Tips: Github Action的工作流可以有多个,通过不同的文件配置不同的trigger即可。

完整事件列表还是去官网看,此处不列出了。

配置好上面的信息后,只需要开始配置你的jobs即可完成Action的定制了。

以使用Gradle构建的Java Application为例,针对其build过程,可以分解为两个step,第一个是安装合适版本的JDK和Gradle,第二个则是通过Gradle构建程序。

于是可以得到下面的Jobs:

1
2
3
4
5
6
7
8
9
10
11
12
13
runs-on: ubuntu-latest
steps:
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: "11"
distribution: "temurin"
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build

针对上面的一些东西进行讲解,可以看到runs-on用于定制运行环境。

同时这里可以看到在安装JDK时,使用了uses: action/setup-java@v3,具体的可以看其使用说明:https://github.com/actions/setup-java,此处仅讲解关键点。

每个step的name可以随意,但是最好见名知意,uses可以指定一个action仓库,一般来说可以到https://github.com/actions里找适合自己的,然后参照使用说明配置`with`项。

这里的actions/setup-java@v3就是用于安装JDK的。

随后的step都仅仅是执行命令,第二个stepgradle赋予了执行权限,随后第三个step调用了gradle构建了程序。

在这里其实还有一个问题,即代码从哪儿来?

一般来说会在steps的第一个step配置代码,使用的是actions/checkout@v3,完整的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
name: Android CI

on:
push:
branches: [master]
pull_request:
branches: [master]

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"
distribution: "temurin"
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build

上面的只配置了一个job即构建,实际上可以加上test等不同需求的job

定制工作流 - Github Release自动推送

在这里会通过定义多个job来实现构建与Github Release的推送。

以构建安卓程序来说,其构建的step可以定制为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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"
distribution: "temurin"
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build


为了自动上传构建后的文件到Github Release,我们需要添加一个Upload Action,如下:

1
2
3
4
5
- name: Upload Release APK
uses: actions/upload-artifact@v3
with:
name: AndroidAppliacation-Release
path: app/build/outputs/apk/release/app-release-unsigned.apk

接下来定制release job,首先Github Release实际根据Tag进行分类,

那么首先先获取对应的Tag Name

1
2
3
4
5
- name: Prepare Release
id: prepare_release
run: |
TAG_NAME=`echo $GITHUB_REF | cut -d / -f3`
echo ::set-output name=tag_name::$TAG_NAME

随后问题来了,怎么获取上一个job的构建程序呢?

要注意每个job都是运行在独立的环境中的,于是需要对一个job进行调整,使其上传对应的构建好的文件:

1
2
3
4
5
- name: Upload Release APK
uses: actions/upload-artifact@v3
with:
name: AndroidApp-Release
path: app/build/outputs/apk/release/app-release-unsigned.apk

然后在第二个job中下载该APK:

1
2
3
4
5
- name: Download Release APK
if: steps.prepare_release.outputs.tag_name
uses: actions/download-artifact@v2
with:
name: AndroidApp-Release

请注意name字段的对应关系。

可以看到这里实际上还配置了if,只有在获取tag_name成功时才会执行此步。

随后创建Github Release

1
2
3
4
5
6
7
8
9
10
11
- name: Create Release
id: create_release
if: steps.prepare_release.outputs.tag_name
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
tag_name: ${{steps.prepare_release.outputs.tag_name}}
release: Release ${{steps.prepare_release.outputs.tag_name}} by Evalexp
draft: false
prerelease: false

这里的GITHUB_TOKEN是自己获取的,无需自己进行配置。

注意steps.prepare_release.outputs.tag_name实际上是第一个step的输出,在使用中可以通过echo ::set-output name=key::value设置键值对,然后在其他step中通过上述手段获取。

最后,将对应的APK上传至Github Release中:

1
2
3
4
5
6
7
8
9
10
11
- name: Upload Release Assets
id: upload_release_assets
if: steps.create_release.outputs.upload_url
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
upload_url: ${{steps.create_release.outputs.upload_url}}
asset_path: ./app-release-unsigned-${{steps.prepare_release.outputs.tag_name}}.apk
asset_name: app-release-unsigned-${{steps.prepare_release.outputs.tag_name}}.apk
asset_content_type: application/vnd.android.package-archive

至此就配置完成了。

附完整配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
name: Android Release

on:
push:
tags: [v*]

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"
distribution: "temurin"
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build

- name: Upload Release APK
uses: actions/upload-artifact@v3
with:
name: AndroidApp-Release
path: app/build/outputs/apk/release/app-release-unsigned.apk

release:
needs: build

runs-on: ubuntu-latest

steps:
- name: Prepare Release
id: prepare_release
run: |
TAG_NAME=`echo $GITHUB_REF | cut -d / -f3`
echo ::set-output name=tag_name::$TAG_NAME
- name: Download Release APK
if: steps.prepare_release.outputs.tag_name
uses: actions/download-artifact@v2
with:
name: AndroidApp-Release

- shell: bash
run: |
mv app-release-unsigned.apk app-release-unsigned-${{steps.prepare_release.outputs.tag_name}}.apk
- name: Create Release
id: create_release
if: steps.prepare_release.outputs.tag_name
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
tag_name: ${{steps.prepare_release.outputs.tag_name}}
release: Release ${{steps.prepare_release.outputs.tag_name}} by Evalexp
draft: false
prerelease: false

- name: Upload Release Assets
id: upload_release_assets
if: steps.create_release.outputs.upload_url
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
with:
upload_url: ${{steps.create_release.outputs.upload_url}}
asset_path: ./app-release-unsigned-${{steps.prepare_release.outputs.tag_name}}.apk
asset_name: app-release-unsigned-${{steps.prepare_release.outputs.tag_name}}.apk
asset_content_type: application/vnd.android.package-archive

持续集成 - CI

在有了上面的基础知识后,就可以进入到今天的主题了,即使用Github Action进行CI/CD,实际上Github Action主要还是进行的CI而不是CD

以常规的静态博客为例,我使用的是Hexo,这是一个基于NodeJS的静态博客生成框架,那么对于静态博客的生成来说,其构建步骤较为简单,给出workflow如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
name: Blog CI

on:
push:
tags:
- "*-build"

jobs:
build:
name: Build Docker image and auto deploy
runs-on: ubuntu-latest

steps:
- name: Check out
uses: actions/checkout@v2

- name: Get Tag
id: meta
uses: docker/metadata-action@v3
with:
images: |
registry.cn-shanghai.aliyuncs.com/evalexp-private/blog

- name: Setup Nodejs
uses: actions/setup-node@v3
with:
node-version: 16

- name: Install Hexo
run: npm install hexo -g

- name: Install dependencies
run: npm install

- name: Generate Blog
run: hexo g

上面唯一需要解释一下的就是第二个step了,这个是docker官方提供的从Git refs提取元数据的Action,比较方便。其中images字段是Tagbase name

注意上面其实就已经将博客正常构建完成了,接下来是将其进行Docker镜像的打包,对于静态博客,打包比较简单,只需要通过Nginx镜像的定制即可,Dockerfile如下:

1
2
FROM nginx
COPY public /usr/share/nginx/html

随后使用Docker官方的Action构建并推送到远程仓库。

由于Docker Hub国内基本访问龟速,因此这里使用了阿里云的镜像服务,个人版有100个镜像仓库容量,比较推荐。

在推送前需要进行登陆操作:

1
2
3
4
5
6
- name: Login Registry
uses: docker/login-action@v1
with:
registry: registry.cn-shanghai.aliyuncs.com
username: ${{ secrets.ALIYUN_USER }}
password: ${{ secrets.ALIYUN_PASSWORD }}

这里需要注意,这里的secrets需要自己在项目的Settings中配置才能使用。

随后根据Dockerfile构建推送:

1
2
3
4
5
6
7
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

附完整的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
name: Blog CI

on:
push:
tags:
- "*-build"

jobs:
build:
name: Build Docker image and auto deploy
runs-on: ubuntu-latest

steps:
- name: Check out
uses: actions/checkout@v2

- name: Get Tag
id: meta
uses: docker/metadata-action@v3
with:
images: |
registry.cn-shanghai.aliyuncs.com/evalexp-private/blog

- name: Setup Nodejs
uses: actions/setup-node@v3
with:
node-version: 16

- name: Install Hexo
run: npm install hexo -g

- name: Install dependencies
run: npm install

- name: Generate Blog
run: hexo g

- name: Login Registry
uses: docker/login-action@v1
with:
registry: registry.cn-shanghai.aliyuncs.com
username: ${{ secrets.ALIYUN_USER }}
password: ${{ secrets.ALIYUN_PASSWORD }}

- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

持续部署 - CD

基于容器化的持续部署其实比较简单,常见的方案就两种:

  • Webhook
  • Watch

两种方案其实理论上Webhook会更好一点,只需要在服务器上启动一个Webhook服务,Github Action构建完成后通过Webhook通知服务器拉取最新镜像重新通过新镜像启动容器即可自动部署,但是目前来说该方案还没有一个成熟的实践,因此还是采用了第二种,即Watch方式。

Watch方式实际上是通过一定时间间隔的轮询镜像是否更新,如果有则停止容器并且拉取最新镜像,这种方式无需Github方面有任何配置,也算是一种优点了。

此处采用的是watchtower,这里我只对个人的博客以及cyberchef(传入的参数应该是容器名,因此建议容器名自定义)进行了watch,轮询时间为30秒:

1
2
3
4
5
6
7
8
9
10
11
12
version: "3"
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /home/evalexp/.docker/config.json:/config.json
command: --interval 30 cyberchef blog
logging:
options:
max-size: "5m"

此时通过推送最新博客的sourceGithub触发构建,即可完成整套CI/CD流程。

效果如下,对于Github Action

image

前往阿里云查看可以发现:

image

已经构建推送成功。

Watchtower的日志输出:

1
watchtower    | time="2022-11-12T14:38:25Z" level=info msg="Session done" Failed=0 Scanned=2 Updated=1 notify=no

刷新博客,可以发现已经成功更新了。