Ubuntu 运维系列 | 第 27 天:CI/CD 运维实践——GitLab CI 与自动化发布流水线

引言

在 Ubuntu 运维的日常中,代码与配置从提交到上线,往往经历”手动 scp + 手动重启”的高风险过程。一旦人工操作出现疏漏,要么版本不一致、要么配置错乱,甚至直接引发线上故障。CI/CD(持续集成 / 持续交付 / 持续部署)正是为此而生:它把”构建、测试、部署、回滚”串成一条可重复、可审计、可回退的流水线,让运维工作从”救火队员”转型为”流水线架构师”。

今天我们将聚焦 GitLab CI —— 当前最普及的自托管 CI/CD 平台之一 —— 在 Ubuntu 上完成从”代码提交”到”服务上线”的全链路自动化,涵盖 Runner 部署、流水线编排、制品管理、安全扫描与自动回滚,并给出可直接复用的 .gitlab-ci.yml 模板与故障排查手册。

Ubuntu 运维 第27天

一、CI/CD 核心概念与流水线分层

1.1 什么是 CI 与 CD

CI(Continuous Integration) 关注”代码提交后是否仍然可用”:每次合并触发构建与测试,尽早发现集成问题。
CD(Continuous Delivery / Continuous Deployment) 关注”通过测试的构建产物如何可靠地上线”:Delivery 需要人工审批,Deployment 全自动。

在运维语境中,二者共同构成一条 流水线(Pipeline),由若干 阶段(Stage) 串联而成,典型分层如下:

commit -> lint -> build -> unit-test -> package -> scan -> deploy-staging -> deploy-prod -> rollback-hook

1.2 关键组件

组件 作用 运维关注点
GitLab Server Web + 数据库 + 任务调度 高可用、备份、升级窗口
GitLab Runner 实际执行 Job 的代理 注册到 Runner Manager、并发数、资源限额
Registry Docker 镜像仓库 镜像保留策略、GC、签名
Artifacts 构建产物暂存 保留周期、下载权限
Variables 密钥与环境变量 Masked、Protected 范围

1.3 流水线设计原则

  • 幂等性:同一 Job 重跑结果一致,避免”偶发通过”。
  • 最短反馈:lint/unit-test 放在最前,失败即刻阻断。
  • 环境隔离:staging 与 prod 通过 environment 关键字隔离,禁止跨环境部署。
  • 可追溯:每个部署标记 Git SHA 与构建号,便于回滚。

二、在 Ubuntu 上部署 GitLab Runner

2.1 安装 GitLab Runner

GitLab Runner 官方提供 Debian/Ubuntu 安装脚本,支持 Shell 注册、Docker 注册、Kubernetes 注册三种模式。推荐采用 Docker executor,以容器隔离降低宿主机污染风险。

# 添加官方 APT 源
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get update
sudo apt-get install -y gitlab-runner

# 启动并设置开机自启
sudo systemctl enable --now gitlab-runner
sudo systemctl status gitlab-runner --no-pager

2.2 注册 Runner 到 GitLab 实例

Runner 注册需要 runner registration token(在 GitLab 管理后台 → Settings → CI/CD → Runners 中获取):

sudo gitlab-runner register 
  --url "https://gitlab.example.com/" 
  --registration-token "glrt-xxxxxxxxxxxxxx" 
  --executor "docker" 
  --description "ubuntu-ci-01" 
  --tag-list "ubuntu,docker,linux" 
  --docker-image "ubuntu:24.04" 
  --docker-shell "/bin/bash" 
  --locked "true" 
  --limit "1"

注册完成后,/etc/gitlab-runner/config.toml 会新增一段 Runner 配置,核心字段说明:

[[runners]]
  name = "ubuntu-ci-01"
  url = "https://gitlab.example.com/"
  token = "glrt-xxxxxxxxxxxxxx"
  executor = "docker"
  [runners.docker]
    image = "ubuntu:24.04"
    privileged = false
    disable_entrypoint_overwrite = true
    shm_size = 0
    pull_policy = ["if-not-present"]

2.3 Runner 资源限额与安全

生产环境的 Runner 不应”任意运行任意代码”。建议在 config.toml 中加上以下硬限制:

[runners.docker]
  privileged = false
  disable_entrypoint_overwrite = true
  allowpull = ["docker.io/library/*", "registry.gitlab.com/*"]
  pull_timeout = 120
  stop_timeout = 10
  [runners.docker.volumes]
    allowed = []

同时在 GitLab 项目侧使用 Protected Variables(需受保护分支才能读到)、CI/CD Variables(Masked 隐藏明文)、以及 .gitlab-ci.ymlrules: 限定触发分支,形成三道防线。

三、编写 .gitlab-ci.yml 实战模板

3.1 模板总览

以下模板适用于 Ubuntu 上部署的 Web 服务(Nginx + Golang/Python/Node 等均可),包含静态检查、构建、单测、镜像打包、安全扫描、staging 部署与 prod 部署六个阶段:

stages:
  - lint
  - build
  - test
  - package
  - scan
  - deploy

variables:
  IMAGE_NAME: registry.gitlab.com/yourgroup/yourapp
  IMAGE_TAG: $CI_COMMIT_SHORT_SHA
  STAGING_HOST: 10.0.1.10
  PROD_HOST: 10.0.2.20

# 通用:仅在主分支或带 deploy:* 标签时触发部署
.deploy_rules:
  - if: '$CI_COMMIT_BRANCH == "main"'
  - if: '$CI_COMMIT_TAG =~ /^v.*$/'

3.2 各阶段 Job 细节

lint:
  stage: lint
  image: golangci/golangci-lint:latest
  script:
    - golangci-lint run ./... --timeout 3m
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

build:
  stage: build
  image: golang:1.22-bookworm
  script:
    - go build -o app ./cmd/server
  artifacts:
    paths: [app]
    expire_in: 1 day

test:
  stage: test
  image: golang:1.22-bookworm
  script:
    - go test ./... -race -coverprofile=coverage.out
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage-cobertura.xml

package:
  stage: package
  image: docker:24
  services:
    - docker:24-dind
  script:
    - apk add --no-cache ca-certificates git
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker push $IMAGE_NAME:$IMAGE_TAG
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

scan:
  stage: scan
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker run --rm -v "$PWD:/repo" aquasec/trivy image $IMAGE_NAME:$IMAGE_TAG
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

3.3 自动部署与回滚

部署阶段使用 environment 隔离环境,并通过 GitLab 内置的 Auto-DeployRunners 实现 SSH 推送:

deploy-staging:
  stage: deploy
  image: alpine:3.20
  script:
    - apk add --no-cache openssh-client bash
    - |
      ssh -o StrictHostKeyChecking=no $STAGING_USER@$STAGING_HOST "
        docker pull $IMAGE_NAME:$IMAGE_TAG &&
        docker tag $IMAGE_NAME:$IMAGE_TAG myapp:latest &&
        docker service update --image myapp:latest myapp ||
        systemctl restart myapp.service
      "
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy-prod:
  stage: deploy
  image: alpine:3.20
  script:
    - apk add --no-cache openssh-client bash
    - |
      ssh -o StrictHostKeyChecking=no $PROD_USER@$PROD_HOST "
        docker pull $IMAGE_NAME:$IMAGE_TAG &&
        docker tag $IMAGE_NAME:$IMAGE_TAG myapp:latest &&
        docker service update --image myapp:latest myapp
      "
  environment:
    name: production
    url: https://example.com
  when: manual          # 生产环境要求人工确认
  allow_failure: false
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

3.4 自动回滚 Hook

在部署脚本中保留”上一版本镜像”,一旦健康检查失败即回滚:

#!/usr/bin/env bash
# deploy-with-rollback.sh  —— 放在 Runner 或目标机器
set -euo pipefail

NEW_IMG="myapp:$CI_COMMIT_SHORT_SHA"
PREV_FILE="/var/lib/myapp/previous-image"
PREV_IMG=""

if [ -f "$PREV_FILE" ]; then PREV_IMG=$(cat "$PREV_FILE"); fi

docker pull "$NEW_IMG"
docker tag "$NEW_IMG" myapp:latest
docker service update --image myapp:latest myapp >/dev/null

# 等待 60s 并检查健康状态
sleep 60
if ! curl -fsS http://localhost/healthz; then
  echo "Health check failed, rolling back to $PREV_IMG"
  if [ -n "$PREV_IMG" ]; then
    docker service update --image "$PREV_IMG" myapp
  fi
  exit 1
fi
echo "$NEW_IMG" > "$PREV_FILE"

四、安全扫描与 DevSecOps 集成

GitLab CI 天然支持 SAST(静态代码分析)、DAST(动态扫描)、SCA(依赖扫描)、Container Scanning。通过 GitLab 内置的 include 即可启用,无需额外部署:

include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml
  - template: Security/Container-Scanning.gitlab-ci.yml
  - template: Security/Dependency-Scanning.gitlab-ci.yml

stages:
  - build
  - test
  - package
  - security
  - deploy

sast:
  stage: security
  needs: ["package"]

container_scanning:
  stage: security
  needs: ["package"]

扫描结果会以 Issues + Code Owners 的方式回写 GitLab,关键级别(Critical/High)会自动阻断 deploy-prod

五、常见问题与排查

5.1 Runner 注册后一直显示 “Requesting job…”

通常原因为网络不通或 token 不匹配:

# 1. 检查 Runner 是否能访问 GitLab
curl -I https://gitlab.example.com
# 2. 查看 Runner 日志
sudo journalctl -u gitlab-runner -n 100 --no-pager
# 3. 手动重新注册
sudo gitlab-runner reconfigure --url ... --token ...

5.2 Docker-in-Docker (dind) 拉取镜像超时

services: [docker:dind] 启动的 dind 容器需要额外 30–60 秒初始化。可通过以下手段缓解:

  • 在 Job 开始处加 docker info 等待循环;
  • 使用 pull_policy: ["if-not-present"] 减少重复拉取;
  • 对私有镜像提前 docker login

5.3 Artifacts 过大导致 Pipeline 变慢

artifacts:paths 应尽量只保留必要文件,并对非关键产物设置 expire_in: 1 day。对二进制制品,建议直接 push 到 Registry,而非走 Artifacts 通道。

5.4 部署 SSH 报 “Permission denied”

确认以下三点:
1. known_hosts 是否已包含目标主机(或使用 StrictHostKeyChecking=no);
2. CI/CD Variable 中的 SSH 私钥是否 protected 且未过期;
3. 目标机器 /etc/ssh/sshd_config 是否允许公钥登录。

5.5 回滚未生效

检查:
– 部署脚本是否在健康检查失败时 exit 1
previous-image 文件是否持久化在容器外;
– 容器编排(docker-compose / k8s)是否正确识别镜像标签变化。

六、总结与最佳实践

CI/CD 在 Ubuntu 运维中的价值,不只是”省掉点鼠标”,而是构建出一套 可度量、可回滚、可审计 的交付体系。落地建议按以下顺序推进:

  1. 先有 Runner,后有流水线 —— 先把 Runner 注册、限额、标签体系做好;
  2. 先 staging,后 prod —— staging 全自动化,prod 加人工卡点;
  3. 先构建,后安全 —— 安全扫描在流水线后段加入,避免拖慢开发节奏;
  4. 保留回滚通道 —— 任何部署都需可回退,回滚脚本与部署脚本同等重要;
  5. 可观测联动 —— 部署事件写入 Prometheus/ELK,便于故障定位与 SLA 审计。

当第 26 天的 Prometheus + Grafana 监控告警体系与今日的 CI/CD 流水线打通后,一次部署的生命周期可以完全自动化:代码提交 → 构建 → 测试 → 扫描 → 部署 → 监控 → 告警 → 回滚,运维工作真正从”手工作坊”走向”工业流水线”。

下期预告

明天(第 28 天)我们将进入 故障排查实战——启动失败、高负载、网络异常的定位与处理,从 systemctl statusdmesgtop/htopss/netstattcpdump 等工具入手,构建一套系统的故障定位方法学,让任何”红色告警”都能在 10 分钟内收敛到根因。敬请期待!

微信二维码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容