最后的一只章鱼猫 —— GitHub Actions 实现编译打包 Golang 到 Docker 镜像

最后的一只章鱼猫 —— GitHub Actions 实现编译打包 Golang 到 Docker 镜像

技术 2639 字 / 5 分钟

这阵子还挺忙的,上周二飞天津参加第五空间线下赛,回来后周五又是助手那边的面试,周六又是省赛线下赛,这星期又是协会这边的面试。 忙里偷闲抽了点时间继续在看 Redis,目前已经看到原理篇了,了解到了不少有意思的算法与实现原理,整本书看完后会写点东西总结一下。近期助手的 GitHub 教师认证通过了,并开通了团队的一堆福利。考虑到 GitHub 这边的工单系统更加完善,因而逐渐开始从自建 Gitea 逐步转移到 GitHub。CI 也由原来的 Drone,转到 GitHub Actions。 因此今天就想聊聊 GitHub Actions。它是 GitHub 今年新推出的可用于创建项目自动化构建工作流,说白了就是 GitHub 上也可以做 CI/CD 了。Circle CI 和 Travis CI 瑟瑟发抖 GitHub Actions 对于个人的免费额度是一个月 3000 分钟。对于个人项目足足有余。那么说干就干,来试试用 GitHub Actions 编译 Golang 后构建 Docker 镜像并发布到阿里云镜像仓库。

YML 格式

点击项目 Repo 上方的 Actions 进入面板。在这里可以看到项目的构建情况。

GitHub Actions 面板

可以看到我这里其实有几个失败的构建,这些都是自己的踩坑摸索尝试。 左侧可以看到我这里有一个Build & Deploy的工作流。工作流的配置是存放在项目下的/.github/workflows文件夹中,使用 YAML 进行编写。 基本格式:

name: Build & Deploy
on: [push]
jobs:
	build:
		name: Build
		runs-on: ubuntu-latest
		steps:
			....
	dockerfile:
		....

其中name就是工作流的名称,即这里的Build & Deployon指定了次工作流的触发方式,push是指当有提交被推送时触发。更多的触发事件可参考官方文档:Events that trigger workflows

下面的jobs就是工作流中的各个“工作”了,各个jobs里又有各种的steps

注意,这里的`jobs`与我们一般印象中 CI 的步骤不是很一样。

这里使用 Drone CI 来进行对比说明。

DroneCI 对比

对于 Drone CI 而言,我们在.drone.yml配置文件中的pipeline中设定到构建步骤,之后会按照从上到下进行构建。 而 GitHub Actions 中的 Jobs 则是分布式的,相互独立的。它们可以同时一起运行,甚至是不运行在一个环境里,而是各自拥有各自的虚拟环境。 这里点在 GitHub Actions 官方的文档里有介绍:

Job A defined task made up of steps. Each job is run in a fresh instance of the virtual environment. You can define the dependency rules for how jobs run in a workflow file. Jobs can run at the same time in parallel or be dependent on the status of a previous job and run sequentially. For example, a workflow can have two sequential jobs that build and test code, where the test job is dependent on the status of the build job. If the build job fails, the test job will not run.

而 Jobs 下面又有许多 Steps,这才是我们 Drone CI 中的一个个遵循从上到下进行执行的“步骤”。

使用预制的 Golang 构建

现在我们开始编写自动编译 Golang 代码的配置,这里直接使用 GitHub Actions 的预制构建即可。

name: Build & Deploy
on: [push]
jobs:
	build:
		name: Build
		runs-on: ubuntu-latest
		steps:

		- name: Set up Go 1.12
		uses: actions/setup-go@v1
		with:
			go-version: 1.12
		id: go

		- name: Check out code into the Go module directory
		uses: actions/checkout@v1

		- name: Get dependencies
		 run: |
			go get -v -t -d ./...
			if [ -f Gopkg.toml ]; then
				curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
				dep ensure
			fi

		- name: Build
		  run: |
			go build -v .
			pwd

		- name: Archive production artifacts
		uses: actions/upload-artifact@v1
		with:
			name: drone_test
			path: /home/runner/work/drone_test/drone_test

这里个 Job 里共有四个 Steps,前面三个都是 GitHub Actions 预制的,第四个是我自己加的,这里聊一下第四个配置 —— Artifact。

Artifact

上面说到,GitHub Actions 的 jobs 之间是分布式的,相互独立的。因此上一个 job 中生成的文件不能直接传到下一个 job 中,这其中需要用到一个名为Artifact的东西。你可以把它理解成一块共享的公共空间,第一个 job 把构建好的中间文件上传到Artifact上,下一个 job 再从Artifact中下载。这里使用 GitHub 官方的 Action actions/upload-artifact。 在with中填写该 Action 需要的配置项,这里是要上传的文件的文件名以及路径。之后的 Job 就可以到这个路径下去下载文件。 更方便的是,上传至 Artifact 的文件,都可以在构建结束后在面板手动点击下载。 Artifact 下载

打包 Docker 镜像

接下来的 job 是将已经编译好的 Golang 二进制文件根据 Dockerfile 打包成 Docker 镜像发布到阿里云镜像仓库。 因为 Job 之间是分布式独立执行的,若想让其按顺序执行,可使用needs来设置。

dockerfile:
	name: Build Image
	runs-on: ubuntu-latest
	needs: build
	steps:

	- name: Get artifacts
	uses: actions/download-artifact@master
	with:
		name: drone_test
		path: /home/runner/work/drone_test/drone_test

这里设置了needs: build使得第二个 Job dockerfile需在第一个build Job 之后才能执行。 然后使用官方的actions/download-artifact来下载 Artifact。

二进制文件打包进镜像并发布

我们可以在 GitHub Marketplace 上来搜索 Actions 使用。 这里我原本使用的是jerray/publish-docker-action这个 Action 实现。但其对于镜像的 Tag 无法做到自定义。我是想让每次构建好的镜像都配上基于时间的 Tag,这样可以方便以后进行版本回滚。但是这个 Action 只能根据当前提交的 Git Branch 或 Tag 生成镜像的 Tag。

起初是想通过环境变量动态生成 Tag,但折腾了两三天后依然无果。便开始自己魔改这个 Action。

魔改 publish-docker-action

将项目 Fork 后,我粗略地阅读了下源码。这个 Action 使用 Golang 编写的,虽然对于 Action 的开发一无所知,但是根据代码我大概能推断出哪些是 GitHub Action 的 API。 在helper.go中,有一个resolveSemanticVersionTag函数用来根据 Branch 或 Tag 生成镜像的 Tag。我把其删去,开始编写自己的自定义 Tag 函数:

func getFormatTag(tagFormat string) []string{
	tags := make([]string, 0)

	t := tagFormat
	t = strings.Replace(t, "%TIMESTAMP%", strconv.Itoa(int(time.Now().Unix())), -1) 	// %TIMESTAMP% Timestamp
	t = strings.Replace(t, "%YYYY%", strconv.Itoa(time.Now().Year()), -1)           	// %YYYY% Year
	t = strings.Replace(t, "%MM%", strconv.Itoa(int(time.Now().Month())), -1)       	// %MM% Month
	t = strings.Replace(t, "%DD%", strconv.Itoa(time.Now().Day()), -1)              	// %DD% Day
	t = strings.Replace(t, "%H%", strconv.Itoa(time.Now().Hour()), -1)              	// %H% Hour
	t = strings.Replace(t, "%m%", strconv.Itoa(time.Now().Minute()), -1)              	// %m% Minute
	t = strings.Replace(t, "%s%", strconv.Itoa(time.Now().Second()), -1)              	// %s% Second

	tags = append(tags, t)
	return tags
}

这里将传入的字符串中的格式化符号进行替换,然后返回。 同时在上面的代码中:

tags := make([]string, 0)
// Add `latest` version
tags = append(tags, "latest")
tags = append(tags, getFormatTag(inputs.TagFormat)...)

inputs.Tags = tags

我还设置了当前镜像默认覆盖latest版本的镜像。 这里的TagFormat是我自己新加的一个配置项,用于输入用户自定义的格式化字符串。我们需要在action.yml中声明这个配置:

tag_format:
    description: 'Set the tag format'
    default: '%TIMESTAMP%'

实际上,GitHub 只通过action.yml加载 Action。 这个 Action 实际上就是起了一个 Docker 容器进行处理,因此我还需要将当前修改好的内容,编译为镜像后发布在 DockerHub 上。这里就不再赘述了,由 GitHub 到 DockerHub 就是一键的事。

源码:publish-docker-action

发布!!

最后一步,就是使用我魔改后的 Action 进行发布部署啦~

- name: Build & Publish to Registry
uses: wuhan005/publish-docker-action@master
with:
	username: ${{ secrets.DOCKER_USERNAME }}
	password: ${{ secrets.DOCKER_PASSWORD }}
	registry: registry.cn-hongkong.aliyuncs.com
	repository: registry.cn-hongkong.aliyuncs.com/eggplant/drone-test
	tag_format: "%YYYY%_%MM%_%DD%_%H%%m%%s%"
	auto_tag: true

这里的secrets可在项目的 Setting 里设置。 阿里云镜像仓库

最终配置

name: Build & Deploy
on: [push]
jobs:
	build:
		name: Build
		runs-on: ubuntu-latest
		steps:

	- name: Set up Go 1.12
		uses: actions/setup-go@v1
		with:
			go-version: 1.12
		id: go

		- name: Check out code into the Go module directory
		uses: actions/checkout@v1

		- name: Get dependencies
		run: |
			go get -v -t -d ./...
			if [ -f Gopkg.toml ]; then
				curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
				dep ensure
			fi
		- name: Build
		run: |
			go build -v .
			pwd

		- name: Archive production artifacts
		uses: actions/upload-artifact@v1
		with:
			name: drone_test
			path: /home/runner/work/drone_test/drone_test

	dockerfile:
		name: Build Image
		runs-on: ubuntu-latest
		needs: build
		steps:

		- name: Get artifacts
		uses: actions/download-artifact@master
		with:
			name: drone_test
			path: /home/runner/work/drone_test/drone_test

		- name: Build & Publish to Registry
		uses: wuhan005/publish-docker-action@master
		with:
			username: ${{ secrets.DOCKER_USERNAME }}
			password: ${{ secrets.DOCKER_PASSWORD }}
			registry: registry.cn-hongkong.aliyuncs.com
			repository: registry.cn-hongkong.aliyuncs.com/eggplant/drone-test
			tag_format: "%YYYY%_%MM%_%DD%_%H%%m%%s%"
			auto_tag: true

总结

每一次配 CI 都要踩好多坑啊……不过最后总算是搞定了! 不过还没结束,把镜像发布到阿里云镜像仓库后还要部署到服务器上,同时还要能实现版本回滚。这些我打算是使用 Swarm + Portainer 实现。 但这俩东西还只是听说过,自己还没实际用过。学校图书馆好像有 Swarm 的书,等把手头这本 Redis 的看完就去借!