引言
在 Ubuntu 运维的日常中,代码与配置从提交到上线,往往经历”手动 scp + 手动重启”的高风险过程。一旦人工操作出现疏漏,要么版本不一致、要么配置错乱,甚至直接引发线上故障。CI/CD(持续集成 / 持续交付 / 持续部署)正是为此而生:它把”构建、测试、部署、回滚”串成一条可重复、可审计、可回退的流水线,让运维工作从”救火队员”转型为”流水线架构师”。
今天我们将聚焦 GitLab CI —— 当前最普及的自托管 CI/CD 平台之一 —— 在 Ubuntu 上完成从”代码提交”到”服务上线”的全链路自动化,涵盖 Runner 部署、流水线编排、制品管理、安全扫描与自动回滚,并给出可直接复用的 .gitlab-ci.yml 模板与故障排查手册。

一、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.yml 的 rules: 限定触发分支,形成三道防线。
三、编写 .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-Deploy 与 Runners 实现 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 运维中的价值,不只是”省掉点鼠标”,而是构建出一套 可度量、可回滚、可审计 的交付体系。落地建议按以下顺序推进:
- 先有 Runner,后有流水线 —— 先把 Runner 注册、限额、标签体系做好;
- 先 staging,后 prod —— staging 全自动化,prod 加人工卡点;
- 先构建,后安全 —— 安全扫描在流水线后段加入,避免拖慢开发节奏;
- 保留回滚通道 —— 任何部署都需可回退,回滚脚本与部署脚本同等重要;
- 可观测联动 —— 部署事件写入 Prometheus/ELK,便于故障定位与 SLA 审计。
当第 26 天的 Prometheus + Grafana 监控告警体系与今日的 CI/CD 流水线打通后,一次部署的生命周期可以完全自动化:代码提交 → 构建 → 测试 → 扫描 → 部署 → 监控 → 告警 → 回滚,运维工作真正从”手工作坊”走向”工业流水线”。
下期预告
明天(第 28 天)我们将进入 故障排查实战——启动失败、高负载、网络异常的定位与处理,从 systemctl status、dmesg、top/htop、ss/netstat、tcpdump 等工具入手,构建一套系统的故障定位方法学,让任何”红色告警”都能在 10 分钟内收敛到根因。敬请期待!

















暂无评论内容