记录最近在Github Action配置实践中的几个新的收获
上周给 k8m (opens new window) 配置了 Github Actions 流水线,对自己而言,虽然以往配置过 go 项目的打 release 和 docker 镜像,但这次配置时又引入了一些新的思路方案,对自己的知识也是一个更新,这里做一下记录。
# 缓存的应用
以往的构建中,因为 GitHub 的构建环境没有网络的受限问题,于是并没有使用过缓存,这次是给别人项目贡献代码,所以格外注重构建时间上的体验,因此也研究学习了一下构建缓存的应用。
# Node 项目
因为该项目使用的是 pnpm 构建工具,在 pnpm 官方文档 (opens new window)中对如何构建及使用缓存已经做了介绍,这里把当前实践的配置直接放在下边:
jobs:
build-release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 18 ]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- name: 使用 Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
cache-dependency-path: "ui/pnpm-lock.yaml"
- name: 编译前端
run: |
cd ui
pnpm install
pnpm build
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
其中参数也比较简单,不一一介绍,当流水线完成一次成功的构建之后,就可以在该流水线看到 node_modules
的缓存了:
再次构建时,可以从构建日志中看到已经使用了该缓存:
# Go 项目
同理,go 语言项目在编译的时候,也可以配置 pkg 的缓存,以加快构建速度:
- name: 设置go环境
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
cache-dependency-path: "go.sum"
2
3
4
5
在定义 go 环境时,版本可以基于 mod 文件声明,缓存则可以通过 sum 声明。
同样,在有成功构建之后,下次再次构建,就可以看到缓存会启用了:
# docker 构建缓存
在构建 docker 镜像时,我们也可以应用一些缓存,从而加快构建速度。
一般在 GitHub Action 中,镜像构建使用的是 docker 官方提供的 action:build-push-action (opens new window),我之前也介绍过用法:利用GitHub Actions自动构建项目的docker镜像并发布到DockerHub (opens new window),之前的文章没有介绍缓存的用法,此处算做一个补充。
关于缓存的用法,官方也有文档:Cache management with GitHub Actions (opens new window)
docker 构建的缓存的存放方式,有多种,这里介绍两种,一种是基于镜像的缓存,一种是基于 GitHub Action 提供的缓存机制。
镜像缓存配置如下:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: user/app:latest
cache-from: type=registry,ref=user/app:buildcache
cache-to: type=registry,ref=user/app:buildcache,mode=max
2
3
4
5
6
7
8
9
10
构建之后,会在镜像仓库中生成一个标签为 buildcache
的镜像,后续构建则会先下载这个缓存,如有更新,也会自动再更新该缓存的内容。
Github 缓存配置如下:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: user/app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
2
3
4
5
6
7
8
9
10
两种方式都可以,按照个人喜好选择即可。
# 一些组件
# 添加 UPX
upx
是一个通用的可以将二进制文件大小缩小约 50%-70%
的工具,在开源项目中,在最佳实践中,尽可能优化二进制大小,也是一个应该考量的点,因此我所有的 Go 项目,都会默认使用该工具压缩。
通过在 Action 中添加如下内容,可以直接在构建环境中安装 upx 命令:
- name: 安装 UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
2
3
4
进入到 action 的详情页面,可以看到更多介绍,不过这些参数就够用了,默认会安装最新版本。
# 添加 gox 命令
gox 是一个可以并发交叉编译 Go 语言项目的工具,原项目目前归档不再更新,我这里使用的是 fork 中的一个还在维护的版本。
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
cache-dependency-path: "go.sum"
- run: go install github.com/mitchellh/gox@latest # setup gox
2
3
4
5
在设置好 go 环境之后,就可以直接安装 gox 了。安装好 gox 之后,就可以使用此工具并发编译了,这里也放一个个人目前使用的 Makefile (opens new window)。
# 制品共享
在一些构建环境中,我们可能会把构建拆分成不同阶段。比如 k8m 是一个前后端在同一个仓库的项目,并且也设计成了前端 dist 制品 embed 到后端二进制架构。这个时候,前后端的构建会做一些分段设计,在不同阶段,就会出现制品共享的需求。
# 制品上传
- name: 上传到共享
uses: actions/upload-artifact@v4
with:
name: workspace
path: ui/dist
2
3
4
5
这是 GitHub 官方提供的 action,可以指定路径,把第一阶段的 dist 暂存。更多参数,可到 readme (opens new window) 详看。
# 制品下载
第一构建阶段上传的制品,可在第二阶段,用如下方式下载:
- name: 从共享下载
uses: actions/download-artifact@v4
with:
name: workspace
path: ui/dist
2
3
4
5
注意,需要 name 与上边上传的保持一致。同理,如果想要应用更多参数,可到 readme (opens new window) 查看。
# docker 镜像构建的思路
前提:这里提到的镜像构建,是指常规的构建出 linux/amd64,linux/arm64
两个平台的镜像。
# 思路一
之前,我都是用 build-push-action (opens new window) 提供的能力做的构建,比如以往的项目:go-ldap-admin (opens new window), 对应的 Dockerfile (opens new window),对应的流水线如下:
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
# 所需要的体系结构,可以在 Available platforms 步骤中获取所有的可用架构
platforms: linux/arm64,linux/amd64
# 镜像推送时间
push: ${{ github.event_name != 'pull_request' }}
# 给清单打上多个标签
tags: |
eryajf/go-ldap-admin:latest
eryajf/go-ldap-admin:${{ steps.date.outputs.today }}
2
3
4
5
6
7
8
9
10
11
12
13
通过 action 中声明的不同 platform,然后用 go build -o go-ldap-admin
命令自动识别系统来构建对应系统的二进制。
这种构建思路,存在两个问题:
- 构建 arm 的镜像时,会格外慢,项目越大越明显。此问题我也提了 issue (opens new window)
- 因为 Dockerfile 设计成了分阶段构建,go 的 mod 缓存做起来也不是很方便,如果再加上前端的缓存,配置方案会更加复杂。
# 思路二
最近,我学习到一种新的知识点,可阅读跨平台 (opens new window)这篇文档。
简单来说,就是在构建多平台镜像时,我们会用到 --platform=linux/amd64
类似这样的参数。而在 Dockerfile 中,默认内置了三个变量,分别如下:
TARGETPLATFORM
:解析值为linux/amd64
TARGETOS
:解析值为linux
TARGETARCH
:解析值为amd64
这里做一个简单的 demo 进行验证,有 Dockerfile 如下:
FROM docker.cnb.cool/znb/images/alpine:3.21
ARG TARGETOS
ARG TARGETARCH
ARG TARGETPLATFORM
RUN echo "Target OS: ${TARGETOS}"
RUN echo "Target Arch: ${TARGETARCH}"
RUN echo "Target Platform: ${TARGETPLATFORM}"
2
3
4
5
6
7
8
9
然后分别运行不同平台的参数:
# amd
$ docker build -t eryajf --platform=linux/amd64 .
# arm
$ docker build -t eryajf --platform=linux/arm64 .
2
3
4
5
可以看到不同的结果:
获取到这个知识点之后,我把整个构建的思路做了一下调整:
- 原来的思路是在 dockerfile 中做分阶段构建,务求在里边独立完整地构建,不依赖其他操作。
- 现在的思路是把二进制的构建放在 dockerfile 之外,dockerfile 只需把对应架构的二进制放到对应容器环境即可。
新的思路可以较好地规避以及解决掉思路一中提到的两个问题点:
- 构建慢
- 利用交叉编译,再加上 gox 的加持,构建速度提升非常大。
- 缓存问题
- 在外边构建,比在 Dockerfile 中配置缓存要容易得多。
因此,现在我个人针对 go 语言项目的镜像构建实践,可参考:eryajfctl (opens new window) 项目的 Makefile (opens new window) 和 Dockerfile (opens new window) 以及 Action (opens new window)。
这个项目本身就是一个模板项目,以后我也会把个人理解的 cobra 框架,以及 Github Action 构建 Go 项目的最佳实践,在该项目中体现,欢迎持续关注该项目。

