前言

Go 项目的构建方式有很多,如何能找到一种适合自己团队的呢?

今天分享的就是我自己摸索出来的一些经验,通过 Gitlab 的强大生态,让 Go 项目从开发到部署都变得更加高效!

环境

为了本次实验,我在本地使用 Docker 跑了如下服务:

  • Traefik: 用于服务自动发现并反向代理
  • Gitlab: 用于测试 CI/CD 的流程(需开启 Container Registry)
  • Gitlab Runner: 用于执行 CI/CD 的任务

Traefik 和 Gitlab 的部署,可以参考我之前两篇文章,里面有详细的部署方案!

域名

  • gitlab.test
  • registry.test

因为是内网测试环境,所以所有服务的 HTTPS 证书都是使用 mkcert 自签名的,这也为接下来的基础设施搭建挖了很大一个坑,但通过爬坑也掌握了更多原理!

注册 Runner

在最新版的 Gitlab 中可以访问 https://gitlab.test/admin/runners 进入 Runner 的管理页面:

  • 进入后点击 New instance runner 按钮进行创建
  • 填写 Tags 以及描述信息,然后点击 Create runner
  • 保存好 Gitlab-Runner 的注册 Token

接下来编写 Gtilab Runner 的配置文件以和 docker-compose.yaml 文件:

tree
.
├── README.md
├── config
│   ├── certs
│   │   └── ca.crt          # 自签名证书的 CA 证书
│   └── config.toml         # Gitlab Runner 配置文件 
└── docker-compose.yml

3 directories, 4 files

config/config.toml 文件:

user = "gitlab-runner"
log_level = "error"
log_format = "text"
concurrent = 5
check_interval = 10
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  id = 1
  url = "https://gitlab.test"
  name = "docker-runner"
  token = "glrt-fgsyprdA3oxDJzxQJxbB"
  executor = "docker"
  cache_dir = "/cache"
  builds_dir = "/builds"
  token_obtained_at = 2023-11-22T06:12:41Z
  token_expires_at = 0001-01-01T00:00:00Z
  [runners.cache]
    MaxUploadedArchiveSize = 0
  [runners.docker]
    image = "docker:latest"
    volumes = ["/cache", "/builds", "/var/run/docker.sock:/var/run/docker.sock:ro"]
    shm_size = 0
    privileged = true
    tls_verify = false
    extra_hosts = ["gitlab.test:10.8.10.254", "registry.test:10.8.10.254"]
    network_mtu = 0
    pull_policy = ["if-not-present"]
    network_mode = "traefik"
    tls_cert_path = "/certs"
    disable_cache = false
    oom_kill_disable = false
    allowed_services = ["docker:*"]
    services_privileged = true
    allowed_pull_policies = ["if-not-present"]
    disable_entrypoint_overwrite = false
  • 在 13,15 行将前面获得的 URL 和 Token
  • 在 25 行将 /var/run/docker.sock:/var/run/docker.sock:ro 加入 volumes
  • 在 29 行设置自定义 TLD 的解析,而 10.8.10.254 这个 IP 是我在 traefik 网络中手动为 Traefik 设置的 IP

docker-compose.yaml 文件:

services:
  runner:
    image: gitlab/gitlab-runner:latest
    restart: always
    hostname: runner
    container_name: runner
    extra_hosts:
      - gitlab.test:10.8.10.254
    volumes:
      - ./config:/etc/gitlab-runner
      - runner-cache:/cache
      - runner-builds:/builds
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      TZ: PRC
      CA_CERTIFICATES_PATH: /etc/gitlab-runner/certs/ca.crt
      CI_SERVER_TLS_CA_FILE: /etc/gitlab-runner/certs/ca.crt

volumes:
  runner-cache:
    name: runner-cache
  runner-builds:
    name: runner-builds

networks:
  traefik:
    external: true
  • 7-8 行为 Gitlab Runner 设置 HOST 解析
  • 16-17 行为 Gitlab Runner 请求 https://gitlab.test 域名设置信任证书!
docker compose up -d

[+] Building 0.0s (0/0)                                                             docker:orbstack
[+] Running 1/1
 ✔ Container runner  Started

成功后前往 Gitlab Admin 的 Runner 页面查看 Runner 是否正常:

Gtilab Runner

项目适配

为了模拟真实开发环境,我创建了两个项目组和两个项目,分别是:

主要的配置都在 services/sendbox 这个项目中,其目录结构如下:

tree -a -I '.git|.idea'
.
├── .gitignore
├── .gitlab-ci.yml
├── Dockerfile
├── LICENSE
├── README.md
├── cmd
│   ├── root.go
│   └── serve.go
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
└── main.go

2 directories, 12 files

我在本地使用了 Go Workspace 来引入本地的包:

cat go.work
go 1.21.4

use .

replace gitlab.test/modules/mail => ../modules/mail

Dockerfile

FROM golang:latest AS compiler-source-code

ARG VERSION=latest
ARG CI_JOB_USER=gitlab-ci-token
ARG GITLAB_HOST=gitlab.test
ARG CI_JOB_TOKEN
ARG CA_CERTIFICATE

WORKDIR /go/src/sendbox
RUN echo "machine gitlab.test login $CI_JOB_USER password $CI_JOB_TOKEN" >> ~/.netrc && \
    echo $CA_CERTIFICATE >> /usr/local/share/ca-certificates/ca.crt && \
    update-ca-certificates
ADD . /go/src/sendbox
RUN go env -w GOPRIVATE=gitlab.test && go mod tidy && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w -X gitlab.test/services/sendbox/cmd.version=$VERSION -X gitlab.test/services/sendbox/cmd.commit=$(git rev-parse HEAD)" -o sendbox main.go

FROM ubuntu:latest AS build-docker-image
COPY --from=compiler-source-code /go/src/sendbox/sendbox /usr/local/bin/sendbox
ENTRYPOINT ["/usr/local/bin/sendbox"]
  • 第 10 行: 将身份认证信息写入 ~/.netrc
  • 第 11 行: 将自签名 CA 证书写入系统中(非自签名证书,或者不适用 HTTPS 的可以删除这两行)
  • 第 12 行: 更新系统信任 CA 证书(非自签名证书,或者不适用 HTTPS 的可以删除这两行)
  • 第 14 行: 使用 -ldflags 将版本信息写入项目的变量中,后期做日志分析及版本稳定性分析可能会用到

在构建 Docker 镜像过程中,可以加入基于环境拉取不同私有包的设置。例如在测试环境,可以在执行构建前,先执行 go get gitlab.test/modules/mail@develop,来拉取 develop 分支最新的 Commit,避免私有包打了 Tag 后,测试环境出现问题,又要多次发布私有包!

CI/CD 配置

这里需要注意一下,最新版的 Gitlab 默认使用 .yml 后缀,所以如果要使用 .yaml 会导致 CI/CD 无法获取。如果一定要使用后者的话,可以在 Gitlab 中进行设置:

Default CI/CD configuration file

在上图中 Default CI/CD configuration file 下方修改默认 CI 配置文件名称即可!

.gitlab-ci.yml 配置如下:

default:
  image: docker:latest
  before_script:
    - export    # 不需要打印所有环境变量的话可以删除
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY

variables:
  GITLAB_IP: 10.0.6.3
  CI_JOB_USER: gitlab-ci-token
  GITLAB_HOST: gitlab.test
  CA_CERTIFICATE: $CA_CERTIFICATE
  CONTAINER_VER_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
  CONTAINER_LATEST_TAG: $CI_REGISTRY_IMAGE:latest

stages:
  - build
  - release

build:
  tags:
    - docker
  stage: build
  script:
    - docker build --add-host gitlab.test:"$GITLAB_IP" --pull --tag "$CONTAINER_VER_TAG" --build-arg VERSION="$CI_COMMIT_REF_SLUG" --build-arg CI_JOB_USER="$CI_JOB_USER" --build-arg GITLAB_HOST="$GITLAB_HOST" --build-arg CI_JOB_TOKEN="$CI_JOB_TOKEN" --build-arg CA_CERTIFICATE="$CA_CERTIFICATE" .
    - docker push $CONTAINER_VER_TAG

release:
  only:
    refs:
      - tags
  tags:
    - docker
  stage: release
  script:
    - docker pull $CONTAINER_VER_TAG
    - docker tag $CONTAINER_VER_TAG $CONTAINER_LATEST_TAG
    - docker push $CONTAINER_LATEST_TAG

Gitlab CI 预设环境变量:

  • CI_REGISTRY: Container Registry HOST,这里对应的就是 registry.test
  • CI_REGISTRY_USER: gitlab-ci-token
  • CI_REGISTRY_PASSWORD: 值与 CI_JOB_TOKEN 一致,如需操作其他项目的 Image,参考下面的权限问题章节
  • CI_REGISTRY_IMAGE: 根据项目的命名空间自动生成的,例如我这里是: registry.test/services/sendbox
  • CI_COMMIT_REF_NAME: 提交的名称,如果是分支就是分支名称,如果是 Tag 就是对应的版本,如: master,develop 或 v1.0.0

如果直接将 CI_COMMIT_REF_NAME 用于 Image Tag 可能存在问题,例如当提交的分支是 feature/auth,那么生成的 Image Tag 就是 registry.test/services/sendbox:feature/auth,这显然不符合 Tag 的命名规范,所以这里可以使用另一个环境变量 CI_COMMIT_REF_SLUG,他会将非字母和数字的字符替换为 Slug,例如将 feature/auth 替换为 feature-auth!

更多预设环境变量可以参考 Gitlab 官方文档 GitLab CI/CD variables

  • 第 4 行: 用于打印出 Gitlab CI 预设的所有环境变量,如果你不想去看文档的话可以执行 export,其中敏感信息会被标记为 [MASKED]
  • 第 26 行: --add-host gitlab.test:"$GITLAB_IP" 这个如果 gitlab 不是在本地的话不用配置,因为我这里使用 dnsmasq 将 *.test 这个 TLD 都解析成了 127.0.0.1,这就导致 buildx 中访问 gitlab.test 最终请求的 IP 是 127.0.0.1:443!
  • 第 36-37 行: 再为最新版本打上 latest 标签

权限问题

如果你使用 Personal Token 作为 CI/CD 中的身份认证的话,那么需要这个 Personal Token 的所有者拥有私有包的仓库访问权限。

我这里使用的是 CI_JOB_TOKEN,这个是 Pipeline 生命周期中有效的 Token,并且在 Gitlab 15.9 版本之后,为了安全考虑,不允许跨项目访问。也就是说,在 services/sendbox 项目的 CI/CD 中使用 CI_JOB_TOKEN 拉取 modules/mail 这个私有包的时候,会认证失败,报错提示如下面图中 233 行所示:

Gitlab pipeline failed

出现这个问题你可以选择关闭 Limit Access to the project 功能,或者将要访问的项目添加到下面的 Allow CI job tokens from the following projects to access this project 列表中:

Gitlab CI/CD Settings - Token Access

这样再次执行就可以成功获取私有包的代码了!

总结

到这里项目的构建与分发问题已经很好的解决了,这种 Overwrite DNS + Self-signed 的证书,真的是能折腾死人!

不过经历最艰难的过程后,能让我更加了解 Gitlab 生态中的服务相互间是如何交互的,焉知非福吧。

后面会再分享一下关于 CD 的最佳实践,敬请期待吧…

I hope this is helpful, Happy hacking…