一个实用版本的 Github Actions 持续集成样例

持续集成与发布(CI/CD)对于程序员而言不是难事, 一般企业内部都会提供现成的持续集成模板,特定的项目只需要修改相应参数即可。 这些现成的模板,很多时候就成我们学习CI/CD的最佳教程。本文即通过一个简单 Go 项目提供一个实用版本的 Github Actions 持续集成模板。

同时,分享一下个人关于CI/CD以及Github Actions的简单经验。

CI 与 CD

在容器概念出现以前,CI/CD之间的边界比较模糊。有了容器概念以后,CI 与 CD 边界变得非常明确,即 CI 负责构建容器镜像, CD 负责发布容器镜像.有了 Kubernetes 之后,CI/CD 又再向前走一步,发展出了 Helm,CI 负责生成 Helm Chart, CD 负责发布 Helm Chart.

所以,CI/CD 是就其目的进行划分的。

CI/CD 本地化

实现 CI/CD 的工具平台也非常多,就个人使用过的工具就有:Jenkins、GitLab CI、Google Cloud Build 以及现在的 Github Actions 等等。

在 Github 开放个人私有仓库功能之前,我的个人私有代码是托管在 Google Cloud Source 上,加上 Google Cloud Build 以及 Google Cloud Registry 全家桶,使用起来还是很方便的。美中不足的就是 Google Cloud 上托管代码,对于 Go 程序员而言,需要做一些辅助工作,因为 Google Cloud Source 上的代码仓库的路径不像 github 仓库路径这样标准,必须搭建一个简单的代理,才能实现常规的 Go Get 操作。

如今 Github 开始提供的免费的个人私有仓库,所以打算将代码迁移过来。既然要迁移代码,必然涉及到持续集成(CI)的问题。

Google Cloud Build 与 Github Actions 的语法是非常不一样的,但是,简单迁移了一个代码仓库后,我发现整个过程并不痛苦。原因并不是因为 Github Actions 易于迁移,而是因为,当初实现项目持续集成(CI)时,大部分构建工作并不依赖与平台,所以需要迁移的功能很少,仅仅是一个简单触发命令而已。

这就是我想分享的一点经验,CI/CD 尽量减少对于构建平台本身的依赖,尽可能多的实现本地化模拟。这样做的好处非常明显,既可以方便迁移,还可以减少对于不同平台的学习成本。

所以,在我写的项目中会仍然保留 Makefile 文件。通过 Makefile 文件实现 CI/CD 本地化。

Github Actions 实践

在使用 Github Actions 功能之前,先看一下,Github 默认提供给 Golang 项目的 WORKFLOW 定义.

name: Go

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

jobs:

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:

    - name: Set up Go 1.x
      uses: actions/setup-go@v2
      with:
        go-version: ^1.13
      id: go

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

    - 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 .

    - name: Test
      run: go test -v .

整个过程分为五步:

  1. 安装 Go 环境;
  2. Checkout 代码;
  3. 取项目依赖;
  4. 构建项目;
  5. 项目测试。

这个过程没什么问题,可以及时发现项目是否可以成功构建并通过测试,但它并不实用。

  • 首先, 它没有完成一次真正的持续集成(CI)过程.
  • 其次, 整个过程过度依赖 Github Actions 工作流。

所以,改造是必须的了。对于 Go 语言项目的CI过程,我一直推荐使用多阶段容器构建方式进行构建.

既可以解决 Go 程序的跨平台问题,又可以实现发布镜像的最小化。

首先,通过提供Dockerfile的方式完成本地化构建。

FROM golang:1.14-alpine AS builder
# 按需安装依赖包
# RUN  apk --update --no-cache add gcc libc-dev ca-certificates  
# 设置Go编译参数
ARG VERSION
ARG COMMIT
ARG BUILDTIME
WORKDIR /app
COPY . .
RUN GOOS=linux go build -o main -ldflags "-X github.com/x-mod/build.version=${VERSION} -X github.com/x-mod/build.commit=${COMMIT} -X github.com/x-mod/build.date=${BUILDTIME}"

# 第二阶段
FROM  alpine
# 安装必要的工具包
RUN  apk --update --no-cache add tzdata ca-certificates \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /app/main /usr/local/bin
ENTRYPOINT [ "main" ]

再通过 Makefile命令模拟CI触发:

...

image:
	docker build --build-arg VERSION=${GITTAG} --build-arg COMMIT=${COMMIT} --build-arg BUILDTIME=${BUILD_TIME} -t ${DOCKER_USER}/${PROJECT}:latest .

完整的样例,请参考看我写的 Golang-Github-Action 模板项目。 完成上面代码后,其实整个CI过程就已经本地化了。就可以通过

$: make image

验证构建过程。验证通过后,我只需要将触发过程迁移到 Github Actions 上即可。

现在就样例项目写一个 Github Actions 构建模板:

# This is a basic workflow to help you get started with Actions for Golang application

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
  push:
    branches:
      - master
    tags:
      - "v*"

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2

      - name: Define variables
        run: |
          echo ::set-env name=PROJECT::$(echo "golang-github-action")
          echo ::set-env name=VERSION::$(git describe --tags)
          echo ::set-env name=COMMIT::$(git rev-parse HEAD)
          echo ::set-env name=BUILDTIME::$(date +%FT%T%z)

      - name: Login to docker hub
        uses: actions-hub/docker/login@master
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

      - name: docker build
        run: docker build --build-arg VERSION=${VERSION} --build-arg COMMIT=${COMMIT} --build-arg BUILDTIME=${BUILDTIME} -t ${DOCKER_USERNAME}/${PROJECT}:${IMAGE_TAG} .
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}

      - name: Push to docker hub
        uses: actions-hub/docker@master
        with:
          args: push ${DOCKER_USERNAME}/${PROJECT}:${IMAGE_TAG}
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}

这个过程分为 4 个步骤:

  1. 准备环境变量
  2. 登录 Docker Hub
  3. 构建镜像
  4. 发布镜像到 Docker Hub

在第 3 步中,如果不是因为需要传递参数,可以直接使用make image命令。

这样一个构建模板,其实整个过程是不依赖与特定开发语言的,因为项目的构建过程已经完全本地化了,即通过本地Dockerfile实现。所以,这个模板可以适用范围是很广的,完全与开发语言无关。

Github Actions 变量设置

关于 Github Actions 的基础概念,我想阮一峰的这篇文章GitHub Actions 入门教程讲的很清楚了,我就不在赘述了。

这里单独将 Github Actions 环境变量的设置列出来说一下。

因为,当我们拿到一个 CI 模板之后,首先遇到的就是如何修改参数或是变量值。

在样例项目中,整个 CI 的 JOB 均是运行在同一台 ubuntu-latest 的虚拟机上。所以可以通过设置系统的环境变量来设置相关参数。

如样例中的第一步:

echo ::set-env name=BUILDTIME::$(date +%FT%T%z)

通过这个命令来设置环境变量。

这个命令的发现过程还挺有意思的。因为需要使用 Docker 发布,所以就找到了 actions-hub/docker 操作。

发现其中的参数 ${IMAGE_TAG} 并非 GitHub Actions 默认提供,所以就看了一下actions-hub/docker/login的实现代码

其中就有这样一段代码,

echo ::set-env name=IMAGE_TAG::${IMAGE_TAG}
echo ::set-env name=IMAGE_NAME::${IMAGE_NAME}

所以,也就依葫芦画瓢借来做自己的环境变量定义使用了。

除了自定义的环境变量以外,Github Actions 本身提供默认变量可以参考该文档using environment variables.

对于一些密钥类的设置,则可以通过在项目中增加对应密钥变量的方式进行设置。官方参考文档:creating-and-storing-encrypted-secrets.

样例模板中

- name: Login to docker hub
        uses: actions-hub/docker/login@master
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

变量值DOCKER_USERNAME, DOCKER_USERNAME就来源于密钥设置。这里要注意的是,在 step 中通过 env 定义的变量只能在该 step 中使用,不可以全局使用。

有了以上相关变量设置的知识,一些基本的 GitHub Actions 问题都可以迎刃而解了。

小结

样例模板实现了每次master主分支推送以及tag推送触发容器镜像的构建,需要的同学可以 clone 该样例项目自行测试。

项目地址:Golang-Github-Action