GitLab CI/CD 流水线实战

一、项目架构与流程

复制代码
         开发者                        GitLab CI/CD                          Harbor
  ┌──────────────┐            ┌─────────────────────────┐          ┌──────────┐
  │ 本地git 打tag │──push──▶│ GitLab CE 识别tag         │          │ 镜像仓库  │
  └──────────────┘            │         │                │          │ hb.reg.com│
                              │         ▼                │          └──────────┘
                              │  GitLab Runner(容器化)    │            ▲
                              │   docker:27 工作容器      │            │ push
                              │   git clone → build      │────────────┘
                              │   → push → 部署           │
                              └─────────────────────────┘

组件版本:

组件 版本 作用
GitLab CE 社区版 代码仓库 + CI/CD 调度
GitLab Runner 17.4.0 执行 CI job(docker executor)
工作镜像 docker:27 job 运行容器(含 docker CLI)
Harbor 2.15.2 私有镜像仓库
应用 Flask + gunicorn 演示 Web 服务

二、项目准备

1. 本地 Python 项目(qinyuxing.py

一个极简 Flask 应用,用于演示 CI/CD 构建:

复制代码
from flask import Flask, jsonify
import socket, datetime
app = Flask(__name__)
​
@app.route("/")
def index():
    return f"<h1>qinyuxing-python-app</h1><p>{socket.gethostname()}</p>"
​
@app.route("/healthz")
def healthz():
    return jsonify({"status": "ok"})

2. Dockerfile(关键:入口名要和 Flask 对象匹配)

复制代码
FROM python:3.13-slim
COPY . /app
WORKDIR /app
RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ \
    "gunicorn>=21.2.0" -r requirements.txt
EXPOSE 5000
CMD ["gunicorn", "-b", "0.0.0.0:5000", "qinyuxing:app", "--workers", "4", "--threads", "2"]

⚠️ 坑:CMDqinyuxing:app = 模块名:Flask对象名。如果文件叫 qinyuxing.py、对象叫 app,就写 qinyuxing:app。别写成 app:app(那是 app.py 才是)。

3. .gitlab-ci.yml(简洁版)

复制代码
variables:
  IMAGE_NAME: hb.reg.com/project/$CI_PROJECT_NAME
​
stages:
  - build
  - deploy
​
build_image:
  stage: build
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - docker login hb.reg.com -u admin -p Harbor12345
    - docker build -t $IMAGE_NAME:$CI_COMMIT_TAG .
    - docker push $IMAGE_NAME:$CI_COMMIT_TAG
    - docker rmi -f $IMAGE_NAME:$CI_COMMIT_TAG
​
deploy_image:
  stage: deploy
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - docker pull $IMAGE_NAME:$CI_COMMIT_TAG
    - docker rm -f $CI_PROJECT_NAME || true
    - docker run --name $CI_PROJECT_NAME -d -p 5000:5000 $IMAGE_NAME:$CI_COMMIT_TAG

4. 触发方式:打 tag(不是 push 分支)

复制代码
git add .
git commit -m "init"
git tag v1.0.0
git push origin main
git push origin v1.0.0   # 打tag推送才触发流水线

⚠️ 坑:rules: - if: $CI_COMMIT_TAG 只认 tag 。普通 git push 分支不会触发,必须 git push origin <tag>


三、搭建步骤

步骤1:部署 GitLab Runner(docker executor)

Runner 用容器跑,挂载宿主 docker.socket 实现 docker-outside-of-docker:

复制代码
docker run -d --name gitlab-runner --restart always \
  --add-host gitlab.test.com:192.168.211.251 \
  --add-host hb.reg.com:192.168.211.101 \
  --group-add 995 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /opt/config:/etc/gitlab-runner \
  gitlab/gitlab-runner:v17.4.0

--add-host 给 Runner 主容器注入内网域名解析;--group-add 995 让容器用户有 docker 组权限。

步骤2:注册 Runner(连 GitLab)

复制代码
docker exec -it gitlab-runner gitlab-runner register
# 填 GitLab URL、token、executor=docker、image=docker:27

步骤3:配置 config.toml(extra_hosts 传给 work 容器)

复制代码
[runners.docker]
  image = "docker:27"
  extra_hosts = ["gitlab.test.com:192.168.211.251", "hb.reg.com:192.168.211.101"]

步骤4:根治 Harbor 自签证书(关键!)

Harbor 用 HTTPS 服务,必须让所有客户端(web/runner)信任它的 CA,否则 docker push/pull 报 x509。正确做法是重新生成标准 CA 证书体系(而不是用 insecure 跳过校验):

① 在 Harbor 服务器(.101)生成标准 CA + 服务器证书

复制代码
mkdir -p /data/harbor-cert
# 生成根 CA(CA:TRUE)
openssl genrsa -out /data/harbor-cert/ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -out ca.crt -subj "/C=CN/ST=Chongqing/L=Chongqing/O=Chengke/OU=Harbor/CN=harbor-ca" \
  -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
​
# 生成服务器密钥 + CSR(SAN 含域名和 IP)
openssl genrsa -out hb.reg.com.key 2048
openssl req -new -key hb.reg.com.key -out hb.reg.com.csr \
  -subj "/C=CN/ST=Chongqing/L=Chongqing/O=Chengke/OU=Harbor/CN=hb.reg.com"
​
# 用根 CA 签发服务器证书(加 SAN)
cat > ext.cnf <<'EOF'
[server]
basicConstraints = CA:FALSE
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:hb.reg.com, DNS:localhost, IP:192.168.211.101
EOF
openssl x509 -req -in hb.reg.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out hb.reg.com.crt -days 3650 -sha256 -extfile ext.cnf -extensions server
​
# 验证证书链
openssl verify -CAfile ca.crt hb.reg.com.crt   # 应显示 OK

② 把新证书配置到 Harbor 并重启

复制代码
# 更新 harbor.yml 指向新证书
sed -i 's|certificate: .*|certificate: /data/harbor-cert/hb.reg.com.crt|' /usr/local/harbor/harbor.yml
sed -i 's|private_key: .*|private_key: /data/harbor-cert/hb.reg.com.key|' /usr/local/harbor/harbor.yml
​
# 同步到 nginx 容器实际读取的位置
cp /data/harbor-cert/hb.reg.com.crt /data/harbor/secret/cert/server.crt
cp /data/harbor-cert/hb.reg.com.key /data/harbor/secret/cert/server.key
​
# 重新生成并重启 Harbor
cd /usr/local/harbor && ./prepare && docker-compose down && docker-compose up -d

③ 把根 CA 分发到所有客户端(web .81/.82、runner .252)

复制代码
mkdir -p /etc/docker/certs.d/hb.reg.com
echo "<harbor-ca.crt 的 base64>" | base64 -d > /etc/docker/certs.d/hb.reg.com/ca.crt
# 加入系统信任(curl 等用)
cp /etc/docker/certs.d/hb.reg.com/ca.crt /usr/share/pki/ca-trust-source/anchors/harbor-ca.crt
update-ca-trust
# 重启 docker 生效
systemctl restart docker

关键:服务器证书必须由 CA:TRUE 的根 CA 签发 (而不是自签 CA:FALSE)。客户端只需信任这个根 CA 即可,无需 insecure。验证:openssl verify -CAfile ca.crt 服务器证书 返回 OK。


四、踩坑大全(亲测,按轻重排序)

坑1:Runner 报 client version 1.43 is too old(最卡)

报错

复制代码
Preparation failed: Error response from daemon: client version 1.43 is too old.
Minimum supported API version is 1.44 (docker.go:956)

根因 :GitLab Runner 二进制内置的 docker client 库 API=1.43,连宿主机 dockerd(min API 1.44) 不兼容。 解决:宿主机 dockerd 降最低 API:

复制代码
/etc/systemd/system/docker.service.d/override.conf:
[Service]
Environment="DOCKER_MIN_API_VERSION=1.40"
systemctl daemon-reload && systemctl restart docker

验证docker version 显示 minimum version 1.40

⚠️ 坑:重启 docker 后,Runner 容器要用 docker restart gitlab-runner 重启,否则挂载旧 socket,连到旧 dockerd。

坑2:无法解析主机 gitlab.test.com

根因 :job 的 work 容器(docker:27)没有内网域名映射,git clone 走公网 DNS 失败。 解决

  • Runner 容器加 --add-host(上面的步骤1)

  • config.toml 配 extra_hosts(把 hosts 传给每个 job 的 work 容器)

注意:work 容器不继承 Runner 容器的 --add-host,必须靠 Runner 的 extra_hosts 配置。

坑3:docker push 报 x509: certificate signed by unknown authority

报错

复制代码
failed to fetch oauth token: Post "https://hb.reg.com/service/token":
tls: failed to verify certificate: x509: certificate signed by unknown authority

根因 :Harbor 原来的证书是自签且声明 CA:FALSE (自己签自己但声明"不是 CA")。docker 的 Go TLS 严格校验 CA:TRUE 的根 CA 签发的证书,自签 CA:FALSE 永远验证不过。

解决(根治):重新生成标准证书体系(见上文步骤4):

  • 生成 CA:TRUE 的根 CA → 用它签发服务器证书(SAN 含域名+IP)

  • 更新 harbor.yml + nginx 容器证书 + 重启 Harbor

  • 把根 CA 分发到所有客户端(certs.d + 系统信任 + 重启 docker)

  • 客户端 docker pull 走正式 TLS 校验,无需 insecure

⚠️ 终极真相:自签证书如果 CA:FALSE,docker 的 Go TLS 必然拒绝 (要求证书链末端是 CA:TRUE 的根)。所以要么生成 CA:TRUE 根证书签发,要么用 insecure 跳过------前者是正确根治,后者是临时偷懒

坑4:容器内 /etc/hosts 只读

现象 :job 里 echo "..." >> /etc/hosts 失败 → job exit 1。 根因 :容器启动后 /etc/hosts 只读。 解决 :不要用 echo 写 hosts,改用 Runner 的 extra_hosts 配置 (容器启动时注入)。若脚本里非写不可,用 tee -a(Alpine 下 /etc/hosts 可写的情况才有效,优先靠 extra_hosts)。

坑5:Alpine 命令差异

工作镜像 docker:27 是 Alpine:

  • update-ca-trust 不存在 → 用 update-ca-certificates

  • 精简,可能缺工具,脚本要兼容

坑6:Dockerfile 入口名

CMD ["gunicorn",..., "app:app"] 默认找 app.py。若你的文件是 qinyuxing.py,必须写成 qinyuxing:app

坑7:镜像路径(pull 404 / NOT_FOUND)

现象docker pull hb.reg.com/myapp/myapp:1.0401 Unauthorized / project not found根因 :Harbor 没有 myapp 这个项目 (只有 cicd/k8s/library/project)。你 CI/CD push 到的其实是 project 项目。 解决 :pull 正确的镜像路径 hb.reg.com/project/python-cicd:<tag>(和你 .gitlab-ci.yml 的 IMAGE_NAME 一致)。


五、最终验证

1. 本地手动测试 build + push(在 .252 上)

复制代码
docker build -t hb.reg.com/project/cicdtest:v1 .
docker push hb.reg.com/project/cicdtest:v1   # 最后能看到 digest 即成功

2. GitLab 流水线

打 tag 推送 → GitLab Pipeline 里看到 build/deploy 两个 job 绿勾 → Job succeeded

3. 确认镜像进 Harbor + web 拉取

复制代码
# 在 web 机器上
echo Harbor12345 | docker login hb.reg.com -u admin --password-stdin
docker pull hb.reg.com/project/python-cicd:<tag>   # 成功 = 证书+权限全通

六、正确配置速查

配置
Runner 版本 gitlab/gitlab-runner:v17.4.0
工作镜像 docker:27
Runner extra_hosts ["gitlab.test.com:...", "hb.reg.com:..."]
Harbor CA 根 CA(harbor-ca,CA:TRUE)签发服务器证书
客户端信任 /etc/docker/certs.d/hb.reg.com/ca.crt + update-ca-trust
触发方式 git push origin <tag>
镜像地址 hb.reg.com/project/python-cicd:<tag>
错误镜像名 别用不存在的项目,如 myapp/myapp(Harbor 无此项目)

七、一句话总结

  • 触发 :打 tag 推送(rules: if: $CI_COMMIT_TAG

  • 执行 :GitLab Runner 拉 docker:27 容器跑 login → build → push

  • 最坑:①Runner 内置 docker client 太旧(降 dockerd API)②Harbor 证书自签 CA:FALSE(重新生成 CA:TRUE 根证书)

  • 经验 :Harbor 证书要根治(生成标准 CA 签发),别用 insecure 临时跳过;client 需信任根 CA


本文记录了从零搭建 GitLab CI/CD + Harbor 镜像仓库的完整过程,含 Runner 容器化部署、docker executor 配置、Harbor 自签证书根治(生成 CA:TRUE 根 CA 签发服务器证书)、客户端 CA 分发,以及 7 个真实踩坑(docker client 版本、hosts 解析、x509 证书、只读 /etc/hosts、Alpine 命令差异、Dockerfile 入口名、镜像路径错误)。

相关推荐
gs801402 小时前
告别 CI/CD 误伤与红条:Docker 镜像智能清理与优雅防冲突实战
ci/cd·docker·容器
leeyi5 小时前
DDD 六条铁律:让 CI 替你骂人——go-arch-lint 门禁实战(第104篇)
ci/cd·agent·领域驱动设计
算法大模型备案干货咪6 小时前
《把内容安全做成CI卡点:AIGC合规的工程化落地》
安全·ci/cd·aigc
code 小楊8 小时前
生产级 Agent 评测体系实战:从 12 指标框架到 CI/CD 质量门禁全链路落地
大数据·人工智能·ci/cd
Ningcode_cloud1 天前
什么是CICD? GitLab + Jenkins 持续集成实战部署手册
运维·ci/cd·云原生·容器·gitlab·jenkins
姚永强1 天前
gitlab安装
运维·gitlab
成茂峰1 天前
实战:内外网隔离环境下基于 Jenkins + PowerShell 的自动化 CI 构建与邮件通知方案
ci/cd·自动化·jenkins
liuyicenysabel2 天前
从 0 到 1:一套 GitHub + GHCR + k3s 的全自动 CI/CD 流水线(Flask 项目实战)
ci/cd·flask·github
力江2 天前
GitLab 私有化部署实战 —— 从 Omnibus 到 Docker 的踩坑之旅
docker·容器·gitlab