在 Go CI/CD 中使用 ko

背景

Go 项目用的是 GitLab CI/CD + Kubernetes 执行器。之前一直有点慢,最近用 ko 优化了一下流程,记录一下。

CD 要打包容器镜像并且推送到镜像仓库,因为用的是 Kubernetes 执行器,没办法直接使用机器上的 docker,GitLab 官方给的方案有 dood,dind 和 kaniko。

  • dood 简单理解就是暴露主机的 docker daemon 给容器内的 docker client,但这是非常具有风险的。
  • dind 每次会额外启动一个容器运行 docker daemon,dind 容器通过容器网络 link 访问 docker daemon,但是 docker 需要运行在特权环境下。
  • kaniko 解决了 dind 需要特权模式的问题,支持完整的 Dockerfile 命令,并且有丰富的定制选项,是比较推荐的方式之一。
  • GitLab 里还提到了一个工具叫 buildah,buildah 类似 kaniko,主要用于构建 OCI 镜像,但也能支持 docker 镜像。

kaniko 是我项目之前在用的方案。我的使用场景里,先是做了一个构建环境镜像 builder,包含了 go 和大仓项目依赖的包,镜像大概是 800+MB,GitLab CI 时启动镜像是 gcr.io/kaniko-project/executor:debug,再由 kaniko 执行 Dockerfile,拉取 builder 镜像,执行构建脚本,最后推送镜像。整个流程中最大的问题就是(故意)没有配置 Caching,每次启动都会重新拉取 builder,虽然是在 VPC 网络内拉取,整个过程还是会花费不少时间。还有一些额外的问题,比如我发现在用 multi-stage builds 的时候,filesystem 会 resolve 不止一次。

ko

无意中发现了 ko 这个项目,功能非常契合我的需求,所以决定用 ko 替换 kaniko。

我主要用到的是 ko build,这个命令的功能是:

  • 支持部分 goreleaser 的配置选项,通过配置构建二进制文件
  • 拉取一个镜像作为基础镜像,简单地将二进制文件和资源文件放到对应的目录,输出一个新镜像,
  • 通过选项配置推送镜像到 KO_DOCKER_REPO 指定的镜像仓库

命令参数里需要特别注意的是 --sbom 选项,各个云平台的镜像仓库实现不同,如果无法正常推送,那你可能需要考虑把 --sbom 指定为 none

详细 build 命令文档参考传送门

ko 会从环境变量 KO_CONFIG_PATH 或工作目录下的 .ko.yaml 加载配置

.ko.yaml 例子:

1
2
3
4
5
6
7
8
9
10
defaultBaseImage: gcr.io/distroless/static:nonroot
builds:
- id: demo
dir: ./app/demo
env:
- CGO_ENABLED=1
flags:
- -trimpath
ldflags:
- -X pkg/build.buildid={{.Env.BUILD_ID}}

由 ko 构建的镜像有两个点要注意:

  • 二进制文件会被放在 /ko-app 下,例如 /ko-app/demo,/ko-app 目录已经被添加到 PATH
  • 如果构建目录下有名为 kodata 的目录,那么 kodata 目录中的文件也会被复制到镜像里,容器内应用通过 KO_DATA_PATH 环境变量获得 kodata 路径(大概是 /var/run/ko)

GitLab CI/CD 集成

很重要的一个改变是配置 .gitlab-ci.yml 启动为 builder,但是在这之前我需要把 ko 二进制添加进 builder 镜像。制作 builder 镜像没有环境限制,可以轻松地在 docker build 环节把从二进制直接 ADD 到镜像或者在 builder 镜像构建时直接用 go install github.com/google/ko@latest 直接从源码构建(不要忘记 builder 有 go 环境),构建完成后需要 go clean -modcache。启动镜像是 builder 的优势不仅仅是少了一层镜像的启动,大部分时候 Runner 拉取过的镜像都会保留在 host 上,所以进一步节省了拉取镜像的耗时。

最后在大仓里配置好 .ko.yaml,然后只要在构建脚本中执行 ko build <各种参数> <构建目录> 即可。

.gitlab-ci.yml 例子:

1
2
3
4
5
6
7
8
9
10
11
build:
stage: build
cache: {}
image:
name: my.registry/go/builder:latest
entrypoint: [""]
script:
- cd $CI_PROJECT_DIR
- echo "$CI_REG_PWD" | ko login https://my.registry --username $CI_REG_USER --password-stdin
- export KO_DOCKER_REPO=my.registry/go
- ko build ./app/demo

Github Actions 集成

Github Actions 中集成更加简单,你可以用现成的 imjasonh/setup-ko

.github/workflows/ko-build.yml 例子:

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: build

on:
push:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
env:
KO_DOCKER_REPO: my.registry/go
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: imjasonh/setup-ko@v0.4
with:
version: latest-release
- uses: actions/checkout@v3
- env:
CI_REG_USER: my-user
CI_REG_PWD: ${{ secrets.ci_reg_pwd }}
run: |
echo "$CI_REG_PWD" | ko login https://my.registry --username $CI_REG_USER --password-stdin
ko build ./app/demo

这是结尾

在这次把 kaniko 改成 ko 后,平均构建耗时从 1m30s 降到了 22s,而且避免了不必要的网络流量和文件系统扫描,效果非常满意。

参考