Skip to content

CI/CD 流水线与镜像发布

本篇是 Docker + Go 系列教程的最后一篇。前面四篇我们解决了容器化、镜像优化、多服务编排、生产最佳实践的问题,但整个流程还是手动的:手动构建、手动推镜像、手动部署。本篇将把这一切自动化——通过 CI/CD 流水线,在代码提交后自动构建镜像、扫描漏洞、签名、发布到 Registry,并介绍镜像版本管理策略、多平台发布、私有 Registry 搭建、镜像签名(Cosign)以及滚动更新、蓝绿、金丝雀等部署策略。学完本篇,你将拥有一个从代码到生产的完整自动化闭环。

一、CI/CD 概念简介

1. 什么是 CI/CD

  • CI(Continuous Integration,持续集成):开发者频繁地把代码合并到主分支,每次合并自动运行测试、构建,尽早发现问题。
  • CD(Continuous Delivery/Deployment,持续交付/部署):CI 通过后,自动把应用打包、发布、部署到各个环境。
text
  代码提交        CI 阶段                    CD 阶段
  ┌────┐      ┌──────────┐            ┌──────────┐
  │git │ ───> │ 构建、测试│ ─────────> │ 发布镜像 │ ──> 部署到生产
  │push│      │ 扫描、签名│            │ 推送仓库 │
  └────┘      └──────────┘            └──────────┘

2. CI/CD 的价值

  • 一致性:每次构建都走相同流程,避免「在我机器上能跑」。
  • 快速反馈:提交后几分钟内知道是否破坏了构建。
  • 可追溯:每个镜像对应一个 git commit,可以追溯到代码变更。
  • 安全:自动扫描漏洞,未通过不发布。
  • 减少人为错误:避免手动构建、手动推镜像的低级失误。

3. 常用 CI/CD 工具

工具类型特点
GitHub ActionsSaaS/自托管与 GitHub 深度集成,配置简单
GitLab CI/CDSaaS/自托管GitLab 内置,功能全面
Jenkins自托管老牌,插件丰富,配置复杂
CircleCISaaS速度快,配置简洁
Drone自托管Go 编写,轻量
ArgoCDGitOpsKubernetes 专用 CD 工具

本篇重点讲 GitHub Actions 和 GitLab CI/CD,它们是当前最主流的选择。

二、GitHub Actions 构建 Go 镜像

1. GitHub Actions 基础

GitHub Actions 是 GitHub 内置的 CI/CD 服务。在项目 .github/workflows/ 目录下放 YAML 文件,就会在特定事件(push、PR、release 等)触发执行。

核心概念:

  • Workflow:一个 YAML 文件,定义完整流程。
  • Job:工作流由若干 job 组成,job 之间可以串行或并行。
  • Step:一个 job 由若干 step 组成,按顺序执行。
  • Action:可复用的步骤,类似函数。
  • Runner:执行 job 的机器,GitHub 提供 Ubuntu/Windows/macOS 托管 runner。

2. 准备项目

假设项目结构:

text
myapp/
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
└── .github/
    └── workflows/
        └── docker.yml

main.go(完整可运行):

go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"runtime"
	"time"
)

var (
	version   = "dev"
	commit    = "none"
	buildTime = "unknown"
)

type Info struct {
	Version   string    `json:"version"`
	Commit    string    `json:"commit"`
	BuildTime string    `json:"build_time"`
	GoVersion string    `json:"go_version"`
	Now       time.Time `json:"now"`
}

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		json.NewEncoder(w).Encode(Info{
			Version:   version,
			Commit:    commit,
			BuildTime: buildTime,
			GoVersion: runtime.Version(),
			Now:       time.Now(),
		})
	})

	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("ok"))
	})

	port := os.Getenv("APP_PORT")
	if port == "" {
		port = "8080"
	}
	fmt.Printf("Starting version=%s on port %s\n", version, port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		fmt.Printf("failed: %v\n", err)
		os.Exit(1)
	}
}

Dockerfile:

dockerfile
FROM golang:1.22-alpine AS builder
ENV CGO_ENABLED=0 GOOS=linux
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG VERSION=dev
ARG COMMIT=none
ARG BUILD_TIME=unknown
RUN go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildTime=${BUILD_TIME}" -o server .

FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata && \
    addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /build/server /app/server
ENV TZ=Asia/Shanghai
USER app
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:8080/health || exit 1
ENTRYPOINT ["/app/server"]

3. workflow.yml:自动构建并推送到 GHCR

GHCR(GitHub Container Registry)是 GitHub 提供的镜像仓库,与 GitHub 仓库无缝集成。GitHub Actions 内置 GITHUB_TOKEN,无需额外配置即可推送到 GHCR。

创建 .github/workflows/docker.yml

yaml
name: Build and Push Docker Image

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      security-events: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: "1.22"

      - name: Run tests
        run: |
          go test -v ./...
          go vet ./...

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha,prefix=sha-,format=short
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ steps.meta.outputs.version }}
            COMMIT=${{ github.sha }}
            BUILD_TIME=${{ steps.meta.outputs.date }}

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          format: sarif
          output: trivy-results.sarif
          exit-code: "1"
          severity: "CRITICAL,HIGH"

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

说明:上述 YAML 中 $\{\{ \}\} 是 GitHub Actions 的表达式语法,用于在运行时求值。例如 $\{\{ secrets.GITHUB_TOKEN \}\} 会替换为内置 token,$\{\{ github.sha \}\} 替换为本次提交的完整 commit hash。这些只出现在 YAML 代码块里,由 GitHub Actions 解析。

4. workflow 关键点解读

触发条件

yaml
on:
  push:
    branches: [main] # 推到 main 分支触发
    tags: ["v*"] # 推 v 开头的 tag 触发(如 v1.0.0)
  pull_request:
    branches: [main] # 针对 main 的 PR 触发

权限:推送 GHCR 需要 packages: write,上传扫描结果需要 security-events: write

metadata-action:自动根据触发事件生成镜像标签。例如:

  • push 到 main:生成 mainlatest 标签。
  • push tag v1.2.3:生成 1.2.31.21 标签。
  • PR #5:生成 pr-5 标签。
  • 每次:生成 sha-abc1234 标签(commit 短 hash)。

缓存cache-from: type=ghacache-to: type=gha,mode=max 用 GitHub Actions 缓存加速构建,第二次构建会快很多。

Trivy 扫描:构建后扫描镜像,发现 CRITICAL 或 HIGH 漏洞则 exit-code: 1 让 job 失败。结果以 SARIF 格式上传到 GitHub Security 标签页。

5. 验证流水线

提交代码后,在 GitHub 仓库的 Actions 标签页可以看到运行情况。成功后镜像会出现在右侧 Packages 区域,地址类似:

text
ghcr.io/<用户名>/<仓库名>:latest
ghcr.io/<用户名>/<仓库名>:1.2.3

拉取测试:

bash
docker pull ghcr.io/<用户>/<仓库>:latest
docker run --rm -p 8080:8080 ghcr.io/<用户>/<仓库>:latest
curl http://localhost:8080

三、GitLab CI/CD 集成

GitLab CI/CD 是 GitLab 内置的 CI/CD 工具,配置文件是项目根目录的 .gitlab-ci.yml

1. .gitlab-ci.yml 编写

yaml
stages:
  - test
  - build
  - scan
  - deploy

variables:
  IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest

# Go 测试
test:
  stage: test
  image: golang:1.22-alpine
  script:
    - go test -v ./...
    - go vet ./...

# 构建并推送镜像
build:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - docker build
      --build-arg VERSION=$CI_COMMIT_TAG
      --build-arg COMMIT=$CI_COMMIT_SHORT_SHA
      --build-arg BUILD_TIME=$CI_JOB_STARTED_AT
      -t $IMAGE
      -t $IMAGE_LATEST
      .
    - docker push $IMAGE
    - docker push $IMAGE_LATEST
  only:
    - main
    - tags

# 漏洞扫描
scan:
  stage: scan
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity CRITICAL,HIGH $IMAGE
  only:
    - main
    - tags

# 部署到 staging
deploy_staging:
  stage: deploy
  image: alpine:3.19
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
    - ssh $DEPLOY_USER@$STAGING_HOST "docker pull $IMAGE_LATEST && docker restart myapp"
  environment:
    name: staging
  only:
    - main

2. GitLab CI 关键点

  • stages:定义阶段顺序,前一阶段全成功才进入下一阶段。
  • variables:内置变量如 $CI_REGISTRY_IMAGE(项目镜像地址)、$CI_COMMIT_SHORT_SHA(短 commit hash)、$CI_COMMIT_TAG(tag 名)。
  • services: docker:24-dind:Docker in Docker,让 job 可以运行 docker 命令。
  • $CI_REGISTRY_PASSWORD:GitLab 内置 token,无需手动配置即可登录内置 Registry。
  • only/except:控制 job 在哪些分支/tag 触发。
  • environment:声明部署环境,GitLab 会有专门的 Environments 视图。

3. GitLab Container Registry

GitLab 自带容器仓库,地址形如 registry.gitlab.com/<组>/<项目>。在项目 Settings → Container Registry 中查看。拉取需要登录:

bash
docker login registry.gitlab.com -u <用户> -p <token>
docker pull registry.gitlab.com/<>/<>:latest

四、镜像版本管理策略

镜像标签是版本管理的核心,直接影响部署的可追溯性和回滚能力。

1. 语义化版本(Semantic Versioning)

格式:MAJOR.MINOR.PATCH

  • MAJOR:不兼容的 API 变更。
  • MINOR:向后兼容的新功能。
  • PATCH:向后兼容的 bug 修复。

对应到 git tag 和镜像标签:

bash
git tag v1.2.3
git push origin v1.2.3
# CI 自动构建出镜像 myapp:1.2.3、myapp:1.2、myapp:1

通常同时打三个标签,方便不同粒度的引用:

  • myapp:1.2.3:精确版本,生产部署用这个。
  • myapp:1.2:指向 1.2.x 的最新,方便小版本自动升级。
  • myapp:1:指向 1.x.x 的最新,方便大版本内自动升级。

2. latest 标签策略

latest 是个有争议的标签。建议的策略:

  • latest 指向 main 分支的最新构建(开发/测试用)。
  • 生产环境绝不使用 latest,必须用精确版本号。
  • 生产环境用 1.2.31.2.3-sha-abc1234 这种可追溯标签。

为什么不用 latest

  • 不可追溯:不知道当前跑的是哪个 commit。
  • 不可回滚:拉取 latest 永远是最新版,回滚困难。
  • 不可预测:今天和明天拉的 latest 可能不同。

3. Git SHA 标签

每次构建打一个基于 commit hash 的标签:

text
myapp:sha-abc1234
myapp:sha-def5678

好处:

  • 精确对应到 commit,方便排查问题。
  • 即使没有打 release tag,也能追踪。
  • 适合开发/staging 环境频繁部署。

4. 推荐的标签组合

一个成熟的镜像应该同时有:

  • myapp:1.2.3:精确版本。
  • myapp:1.2.3-sha-abc1234:版本 + commit,可追溯。
  • myapp:1.2:minor 最新。
  • myapp:1:major 最新。
  • myapp:latest:main 最新(仅开发用)。

5. 不可变标签

进阶做法:让标签一旦推送就不可变(Immutable Tags)。Docker Hub 等仓库默认允许覆盖同名标签,这有风险——同一标签可能指向不同内容。一些企业 Registry(如 Harbor)支持设置某标签不可变,防止覆盖。

五、多平台镜像发布

1. buildx + push

在 CI 中用 buildx 构建多平台镜像,前面讲过本地用法。GitHub Actions 中的写法:

yaml
jobs:
  build-multi:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push multi-arch
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:multi
          cache-from: type=gha
          cache-to: type=gha,mode=max

setup-qemu-action 安装 QEMU 模拟器,让 x86 runner 可以构建 arm64 镜像。

2. Go 应用的多平台优化

Go 的交叉编译让多平台构建特别高效。在 Dockerfile 中用 --platform=$BUILDPLATFORM 让构建阶段在原生架构运行,通过 GOARCH 交叉编译:

dockerfile
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder

ARG TARGETOS=linux
ARG TARGETARCH

ENV CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH}

WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o server .

FROM --platform=$TARGETPLATFORM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /build/server /server
ENTRYPOINT ["/server"]

这样构建速度比用 QEMU 模拟整个构建过程快得多。

3. 验证多平台镜像

bash
docker manifest inspect ghcr.io/user/repo:multi
# 应该看到 amd64 和 arm64 两个 platform

# 在 arm64 机器上拉取,会自动拿到 arm64 版本
docker pull ghcr.io/user/repo:multi

六、Docker Hub / GHCR / 私有 Registry

1. Docker Hub

Docker Hub 是最大的公共镜像仓库。

bash
# 登录
docker login -u <用户>

# 打标签(必须包含用户名前缀)
docker tag myapp:1.0 <用户>/myapp:1.0

# 推送
docker push <用户>/myapp:1.0

# 拉取
docker pull <用户>/myapp:1.0

注意:免费账户有拉取频率限制(匿名 100 次/6小时,登录 200 次/6小时),生产环境不建议依赖 Docker Hub。

2. GHCR(GitHub Container Registry)

GHCR 与 GitHub 仓库集成,权限管理方便。

bash
# 用 PAT 登录(需要 write:packages 权限)
echo $GITHUB_TOKEN | docker login ghcr.io -u <用户> --password-stdin

# 打标签
docker tag myapp:1.0 ghcr.io/<用户>/myapp:1.0

# 推送
docker push ghcr.io/<用户>/myapp:1.0

GHCR 的优势:

  • 与 GitHub 仓库权限打通,团队成员自动有访问权。
  • 免费,且没有 Docker Hub 的频率限制。
  • 镜像页面与代码仓库关联,方便查看。

3. 私有 Registry 搭建

生产环境通常自建私有 Registry,Harbor 是最流行的选择。

快速启动 Harbor(Docker Compose 方式):

bash
# 下载离线安装包
wget https://github.com/goharbor/harbor/releases/download/v2.9.0/harbor-offline-installer-v2.9.0.tgz
tar xzf harbor-offline-installer-v2.9.0.tgz
cd harbor

# 复制配置模板
cp harbor.yml.tmpl harbor.yml

# 编辑 harbor.yml,主要修改:
# hostname: registry.example.com
# http.port: 80
# harbor_admin_password: 强密码
# data_volume: /data/harbor

# 安装
./install.sh

最简版 Registry(官方 Docker Registry):

yaml
# docker-compose.yml
services:
  registry:
    image: registry:2
    ports:
      - "5000:5000"
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    volumes:
      - registry_data:/data

volumes:
  registry_data:

启动后即可使用:

bash
docker-compose up -d

# 推送
docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0

# 拉取
docker pull localhost:5000/myapp:1.0

带 TLS 和认证的 Registry(生产用):

yaml
services:
  registry:
    image: registry:2
    ports:
      - "443:443"
    environment:
      REGISTRY_HTTP_ADDR: 0.0.0.0:443
      REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
      REGISTRY_HTTP_TLS_KEY: /certs/domain.key
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
    volumes:
      - registry_data:/data
      - ./certs:/certs:ro
      - ./auth:/auth:ro

volumes:
  registry_data:

生成认证文件:

bash
mkdir auth
htpasswd -Bc auth/htpasswd myuser
# 输入密码

七、镜像签名:Cosign

镜像签名解决「这个镜像真的是我们构建的吗?有没有被篡改?」的问题。Cosign 是 Sigstore 项目提供的签名工具,已成为事实标准。

1. 安装 Cosign

bash
# macOS
brew install cosign

# Linux
curl -L -o /usr/local/bin/cosign \
  https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
chmod +x /usr/local/bin/cosign

2. 生成密钥对

bash
cosign generate-key-pair
# 会生成 cosign.key(私钥,保密)和 cosign.pub(公钥,公开)
# 设置 COSIGN_PASSWORD 环境变量作为私钥密码

3. 签名镜像

bash
# 设置密码
export COSIGN_PASSWORD=your-password

# 签名
cosign sign --key cosign.key ghcr.io/user/myapp:1.0

签名会作为 OCI artifact 推送到同一仓库。

4. 验证签名

bash
cosign verify --key cosign.pub ghcr.io/user/myapp:1.0
# 验证通过则输出签名信息
# 失败则报错

5. 在 CI 中自动签名

把私钥存到 GitHub Secrets(名为 COSIGN_PRIVATE_KEY),密码存为 COSIGN_PASSWORD

yaml
- name: Install cosign
  uses: sigstore/cosign-installer@v3

- name: Sign image
  env:
    COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
    COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
  run: |
    echo "$COSIGN_PRIVATE_KEY" > cosign.key
    cosign sign --key cosign.key ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}

6. Keyless 签名(推荐)

Cosign 支持基于 OIDC 的 keyless 签名,无需管理密钥:

bash
cosign sign ghcr.io/user/myapp:1.0
# 会用 GitHub Actions 的 OIDC token 签名
# 验证时用证书身份
cosign verify \
  --certificate-identity https://github.com/user/repo/.github/workflows/docker.yml@refs/heads/main \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/user/myapp:1.0

keyless 是更安全的做法,Sigstore 的公开透明日志(Rekor)会记录所有签名,便于审计。

八、部署策略

镜像构建出来后,如何把它部署到生产?不同策略影响更新的平滑度和回滚速度。

1. 滚动更新(Rolling Update)

逐步替换旧版本实例:先启动一个新实例,健康检查通过后,停掉一个旧实例,重复直到全部替换。

text
旧: [v1][v1][v1][v1]

   [v2][v1][v1][v1]   新启动 v2,停一个 v1

   [v2][v2][v1][v1]

   [v2][v2][v2][v1]

   [v2][v2][v2][v2]

Docker Swarm 和 Kubernetes 默认使用这种策略。优点是无停机、资源利用率平稳;缺点是新旧版本会短暂共存,需要版本兼容。

2. 蓝绿部署(Blue-Green)

准备两套完全相同的环境(蓝、绿),当前生产是蓝,部署新版本到绿,验证后流量一次性切到绿。

text
蓝(v1) ──流量──> 用户        绿(v2) 待命
                    ↓ 切换
蓝(v1) 待命          绿(v2) ──流量──> 用户

优点:切换瞬时、回滚只需切回去;缺点:需要双倍资源。

用 Docker Compose 实现蓝绿:

yaml
services:
  app_blue:
    image: myapp:1.0
    ports:
      - "8081:8080"

  app_green:
    image: myapp:1.1
    ports:
      - "8082:8080"

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro

nginx 配置切换 upstream 即可切流量。

3. 金丝雀发布(Canary)

先把少量流量(如 5%)导向新版本,观察一段时间没问题后逐步增加流量比例。

text
用户 ──┬── 95% ──> v1 (稳定)
       └── 5%  ──> v2 (金丝雀)

优点:风险可控,出问题只影响小部分用户;缺点:实现复杂,需要精细的流量控制。Kubernetes + Istio/Flagger 是常用方案。

4. 策略选择

策略停机时间资源开销回滚速度复杂度适用场景
滚动更新日常发布
蓝绿高(双倍)大版本升级
金丝雀风险高的发布

九、完整示例:GitHub Actions 自动构建发布 Go 应用

下面给出一个完整的、生产级的 GitHub Actions 工作流,整合测试、构建、扫描、签名、多平台发布。

1. 完整 workflow 文件

.github/workflows/release.yml

yaml
name: Release

on:
  push:
    branches: [main]
    tags: ["v*"]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.22"
      - run: go test -v -race -coverprofile=coverage.out ./...
      - run: go vet ./...
      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage.out

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
      security-events: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha,prefix=sha-,format=short
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ steps.meta.outputs.version }}
            COMMIT=${{ github.sha }}
            BUILD_TIME=${{ steps.meta.outputs.date }}

      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      - name: Sign image (keyless)
        env:
          DIGEST: ${{ steps.build.outputs.digest }}
        run: cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ env.DIGEST }}

      - name: Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
          format: sarif
          output: trivy-results.sarif
          exit-code: "1"
          severity: "CRITICAL,HIGH"

      - name: Upload scan results
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

  release-notes:
    needs: build-and-push
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Generate release notes
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          body: |
            ## Docker Image

            ```
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
            ```

            Signed with Cosign (keyless). Verify:
            ```
            cosign verify \
              --certificate-identity https://github.com/${{ github.repository }}/.github/workflows/release.yml@refs/tags/${{ github.ref_name }} \
              --certificate-oidc-issuer https://token.actions.githubusercontent.com \
              ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
            ```

上述 workflow 中的 $\{\{ \}\} 表达式由 GitHub Actions 运行时求值。例如 $\{\{ steps.build.outputs.digest \}\} 引用前一步构建输出的镜像 digest,$\{\{ github.ref_name \}\} 是触发构建的 tag 名(如 v1.2.3)。

2. 流程说明

  1. test job:跑测试、vet、覆盖率。
  2. build-and-push job:依赖 test 通过。
    • 用 QEMU + Buildx 构建多平台镜像(amd64 + arm64)。
    • 推送到 GHCR,自动生成语义化版本标签。
    • 用 Cosign keyless 签名。
    • 用 Trivy 扫描漏洞,HIGH/CRITICAL 失败。
  3. release-notes job:仅 tag 触发时执行,自动生成 GitHub Release,附镜像拉取和验证命令。

3. 使用流程

bash
# 日常开发:推到 main
git push origin main
# CI 自动构建 myapp:latest, myapp:main, myapp:sha-xxxx

# 发布版本:打 tag
git tag v1.2.3
git push origin v1.2.3
# CI 自动构建 myapp:1.2.3, myapp:1.2, myapp:1
# 并创建 GitHub Release

4. 验证发布产物

bash
# 拉取
docker pull ghcr.io/<user>/<repo>:1.2.3

# 验证签名
cosign verify \
  --certificate-identity https://github.com/<user>/<repo>/.github/workflows/release.yml@refs/tags/v1.2.3 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/<user>/<repo>:1.2.3

# 运行
docker run --rm -p 8080:8080 ghcr.io/<user>/<repo>:1.2.3
curl http://localhost:8080/version
# 应返回 version=1.2.3

5. 配置 GitHub Secrets

本 workflow 大部分用内置的 GITHUB_TOKEN,无需额外配置。如果用 keyless 签名,需要确保仓库 Actions 权限开启了「Allow GitHub Actions to create and approve pull requests」和 OIDC。

如果用密钥签名(非 keyless),需要添加 Secrets:

  • COSIGN_PRIVATE_KEY:cosign.key 内容。
  • COSIGN_PASSWORD:私钥密码。

十、CI/CD 最佳实践

1. 流水线设计原则

  • 快速失败:测试在最前面,失败立即停止后续步骤。
  • 幂等性:同一 commit 多次构建应产出相同结果。
  • 最小权限:每个 job 只给需要的权限。
  • 缓存:利用 buildx 缓存、Go 模块缓存加速。
  • 可追溯:镜像标签包含 commit SHA,能追溯代码。

2. 安全建议

  • 不要在镜像里硬编码密钥:用环境变量或 secret 管理工具。
  • 用 GHCR/私有 Registry 而非 Docker Hub:避免频率限制,权限可控。
  • 签名验证:生产部署前验证镜像签名。
  • 扫描门禁:HIGH/CRITICAL 漏洞不通过则阻止发布。
  • 依赖锁定go.sum 锁定依赖版本,避免供应链攻击。

3. 常见问题

Q:CI 构建很慢怎么办?

A:依次检查:

  1. 是否启用了 buildx 缓存(cache-from/cache-to)。
  2. go mod download 是否在 COPY . . 之前(利用缓存)。
  3. 是否构建了不必要的平台(只发布 amd64 可能就够)。
  4. 基础镜像是否太大。

Q:镜像签名验证失败?

A:检查 keyless 签名的证书身份(identity)是否与 workflow 路径匹配,tag 触发的签名身份是 @refs/tags/v1.2.3,main 分支是 @refs/heads/main

Q:Trivy 报漏洞但基础镜像没更新怎么办?

A:可以用 .trivyignore 文件忽略特定 CVE(短期),同时推动基础镜像更新(长期)。distroless 镜像通常更新更快。

十一、小结

本篇是 Docker + Go 系列的收官篇,把容器化的最后一环——自动化流水线——讲透了。

关键要点:

  1. CI/CD 价值:一致性、快速反馈、可追溯、安全。
  2. GitHub Actions:用 docker/build-push-action 一站式构建推送,docker/metadata-action 自动生成标签,Trivy 扫描门禁。
  3. GitLab CI:用 .gitlab-ci.yml 定义阶段,dind 执行 docker 命令,内置 Registry 无缝集成。
  4. 版本管理:语义化版本(1.2.3)+ Git SHA(sha-abc1234)+ latest(仅开发),生产用精确版本。
  5. 多平台发布:buildx + QEMU 构建 amd64/arm64 镜像,Go 交叉编译加速。
  6. 私有 Registry:Harbor(企业级)或官方 registry(轻量),生产环境必上。
  7. 镜像签名:Cosign keyless 签名,Sigstore 透明日志审计,部署前验证。
  8. 部署策略:滚动更新(日常)、蓝绿(大版本)、金丝雀(高风险)。

至此,Docker + Go 系列教程完整覆盖了从基础概念到生产部署的全链路。回顾整个系列:

  • 第一篇:Docker 基础与 Go 容器化入门
  • 第二篇:多阶段构建与镜像优化(856MB → 18MB)
  • 第三篇:Docker Compose 多服务编排
  • 第四篇:容器化 Go 应用最佳实践(十二要素、优雅关停、安全加固)
  • 第五篇:CI/CD 流水线与镜像发布(自动化闭环)

掌握这五篇内容,你已经具备了从零到生产部署一个 Go 容器化应用的完整能力。接下来可以进一步学习 Kubernetes、服务网格(Istio)、可观测性(Prometheus + Grafana + Loki)等云原生生态,向真正的云原生工程师迈进。