GitLab CI/CD 使用指南(小白版)

30 阅读17分钟

GitLab CI/CD 使用指南(小白版)

零、先搞懂:CI/CD 到底是什么?

用快递公司来类比 🏭

想象你开了一家网店,每次有订单,你的流程是:

客户下单 → 仓库打包 → 质检 → 快递发货

但如果你每来一单都自己打包、自己检查、自己发快递,那效率极低。

CI/CD 就是雇佣一个机器人帮你自动干这些事:

概念快递类比实际含义
CI(持续集成)机器人自动打包+质检代码提交后,自动编译、自动测试,确保代码没问题
CD(持续部署)机器人自动发货给客户测试通过后,自动把程序部署到服务器上,用户就能用了

一句话:你只管写代码、推送到 GitLab,剩下的编译、测试、部署全部自动完成。


一、五个核心概念(用流水线工厂来理解)

想象一个汽车制造流水线

                 ┌──────────────────────────────────────┐
  代码推送 ──→     Pipeline(整条流水线)                  
                                                       
                   Stage 1: 冲压车间(必须完成)            
                     ├── Job: 冲压车门   同时干          
                     └── Job: 冲压车顶   同时干          
                                                       
                   Stage 2: 焊接车间(冲压完成才能开始)     
                     └── Job: 焊接车身                   
                                                       
                   Stage 3: 喷漆车间                     
                     └── Job: 喷漆                       
                 └──────────────────────────────────────┘
概念汽车工厂类比大白话解释
Pipeline(流水线)整条生产线一次完整的"代码→部署"过程,由 .gitlab-ci.yml 文件定义
Stage(阶段)冲压→焊接→喷漆大的步骤分组,必须按顺序执行,前一个失败后面就不做了
Job(作业)焊接车身这个具体工作一个具体的任务,同一 Stage 里的多个 Job 各干各的同时进行
Runner(执行者)厂房里的机器人一台真实的机器(你的服务器),负责真正动手干活
Artifacts(制品)造好的半成品车门一个 Job 做出来的东西,传给下一个 Job 继续用

这些概念在文件里长什么样?

来看看你项目中 .gitlab-ci.yml 的对应关系:

stages:                    # ← 定义有哪些车间(Stage)
  - test                   #    只有一个:test 车间

hello-job:                 # ← 这是一个 Job(作业),名字随便取
  tags:                    # ← 指派给哪个机器人(Runner)干
    - ubuntu-24.04
  stage: test              # ← 这个 Job 在 test 车间
  script:                  # ← 要干的活儿:跑这些命令
    - echo "GitLab CI/CD is working"
    - whoami
    - pwd

💡 文件名的由来CI = Continuous Integration(持续集成),yml/yaml 是一种配置文件的格式。


二、.gitlab-ci.yml 文件到底怎么写?

2.0 先学一点点 YAML 语法(5 分钟就够)

.gitlab-ci.yml 用的是 YAML 格式,看懂以下三点就够了:

# ① 键值对:用冒号分隔
name: hello-world          # 键是 name,值是 hello-world

# ② 列表:用短横线
fruits:
  - apple                  # 列表第 1 项
  - banana                 # 列表第 2 项
  - orange                 # 列表第 3 项

# ③ 嵌套:用缩进(2 个空格!不能多不能少)
person:
  name: 张三
  hobbies:
    - 游泳
    - 编程
  address:                 # 又可以继续嵌套
    city: 北京
    street: 长安街

⚠️ 缩进必须用空格,不能用 Tab! 这是新手最容易犯的错误。


2.1 GitLab 如何"读懂"这个文件?

GitLab 读 .gitlab-ci.yml 时遵循一条核心规则

🔑 除了 GitLab 预设的保留字,顶层所有键都被当成 Job 名称

来看看具体是什么意思:

stages:           # ← GitLab 说:"stages 是我认识的保留字,这是全局配置"
  - build
  - test

variables:        # ← GitLab 说:"variables 也是我认识的保留字"
  NAME: world

hello:            # ← GitLab 说:"hello?不认识,那你一定是一个 Job!"
  script: echo "hello"    # 于是 GitLab 按 Job 的规则来解析它
全局保留字(放在最外层,控制整个流水线)
保留字干嘛用的大白话
stages定义有哪些阶段,以及执行顺序"我的流水线分 3 步:先构建、再测试、最后部署"
variables定义全局变量,所有 Job 都能用"所有人的名字都用这个值"
default默认配置,所有 Job 自动继承"没特别说明的话,都用这个 Runner 来跑"
include引入另一个 YAML 文件的内容"这份配置文件太长,拆成几份"
workflow控制整个流水线要不要运行"如果是 MR 就不用跑流水线了"
cache全局缓存,让构建更快"上次下载的依赖包别删,下次还能用"
image全局默认的 Docker 镜像"所有人都用 Go 1.25 这个环境"
before_script每个 Job 开工前先跑的命令"干活前先打扫一下卫生"
after_script每个 Job 干完后跑的命令(哪怕失败了也跑)"干完活把工具收好"
Job 内部保留字(放在 Job 里面,控制这个 Job 的行为)
保留字干嘛用的大白话必须吗?
script要执行的命令"要干的活儿"✅ 必须
stage属于哪个阶段"在哪个车间干活"推荐
tags指定哪个 Runner 来跑"派哪个机器人去"推荐
image这个 Job 用哪个 Docker 镜像"用什么工具包"
variables这个 Job 自己用的变量"我自己的特殊配置"
artifacts产出物,传给后面的 Job"做好的半成品放这儿"
environment部署到哪个环境"送到生产环境还是测试环境"
rules什么条件下才执行"只在 main 分支时才干活"
only/except旧版条件控制(推荐用 rules 代替)"只在 main 分支干,不要在 dev 分支干"
needs要等其他哪些 Job 完成"等冲压车间做完我就开工"
when什么时候执行"等我手动点按钮你再跑"
cache这个 Job 的缓存"我的小工具箱"
retry失败后重试几次"出错了再试两次"
timeout最多跑多久"超过 1 小时就别干了"
allow_failure失败了也没关系"你失败不影响其他人"
dependencies只接收指定 Job 的制品"我只要冲压车间的东西,焊接车间的不要"
coverage从输出中提取测试覆盖率"告诉我测试覆盖了百分之几"


2.2 script — 最核心的关键字

script 是每个 Job 的心脏,里面是你真正想做的事——一系列 Shell 命令。你可以把平时在终端里敲的命令直接写进去。

my-job:
  script:
    - echo "第一步:打印信息"
    - go build -o myapp ./cmd/server/     # 编译 Go 代码
    - go test ./...                        # 运行测试
    - ls -la                               # 看看生成了什么文件

每一行 - xxx 就是一条命令,GitLab 会按顺序逐条执行。如果某条命令报错了(退出码不是 0),整个 Job 就失败了。

常见误区script 里面的命令是列表,每条命令独立执行。下面的写法是错的:

# ❌ 错误:cd 到另一个目录只有那一行生效
script:
  - cd /tmp
  - ls    # 这里 ls 的还是原来的目录,不是 /tmp!

如果需要多条命令共享相同的工作目录,要么写在同一行,要么用 pushd/popd

# ✅ 写法一:用 && 串起来
script:
  - cd /tmp && ls

# ✅ 写法二:GitLab 提供的方式(推荐)
script:
  - |
    cd /tmp
    ls
    echo "还在 /tmp 目录"

💡 用 | 表示多行文本块,里面所有行作为一个整体执行。


2.3 tags — 派哪个 Runner 干活?

Runner 是真正干活的机器。每台 Runner 注册时会给它打标签(tag),就像给机器人贴标签:

Runner A:  标签 [ubuntu-24.04, docker, large-memory]
Runner B:  标签 [macos, ios-build]
Runner C:  标签 [ubuntu-24.04, shell]

tags 就是告诉 GitLab:"找一台贴了这个标签的 Runner 来跑":

build:
  tags:
    - ubuntu-24.04   # GitLab 会去找有 ubuntu-24.04 标签的 Runner

🔍 怎么知道有哪些标签可用? 去 GitLab 页面:Settings → CI/CD → Runners,可以看到所有 Runner 及其标签。


2.4 stage — 在哪个步骤?按什么顺序?

还记得工厂流水线吗?stage 就是告诉 GitLab 这个 Job 在流水线的哪个位置。

stages:                # ① 先定义全局执行顺序
  - 备料
  - 加工
  - 质检
  - 发货

切菜-job:
  stage: 备料          # ② 每个 Job 指定自己在哪一步

炒菜-job:
  stage: 加工          # 这个 Job 要等"备料"阶段全部完成才能开始

执行规则

  • 同一 stage 里的 Job 同时开工(并行)
  • 下一个 stage 必须等上一个全部完成才能开始
  • 上一个 stage 有 Job 失败了,后面的默认不执行

💡 Stage 名称可以随便取! buildtestdeploy 只是大家的习惯叫法,你用中文 构建测试部署,或者 步骤一步骤二 都行。唯一要求:Job 里的 stage: xxx 必须和 stages 列表里的某个名称完全一致。另外,执行顺序由列表的先后位置决定,和名字叫什么无关——写在最前面的先执行。


2.5 variables — 用变量,少写重复内容

变量让你在一处定义值,到处引用。GitLab 有两类变量:

# ① 你自己定义的变量
variables:
  APP_NAME: "my-cool-app"      # 定义一个变量
  DEPLOY_USER: "admin"

deploy:
  script:
    - echo "正在部署 ${APP_NAME}"    # 引用变量用 ${变量名} 或 $变量名
    - scp bin/${APP_NAME} ${DEPLOY_USER}@server:/opt/

# ② GitLab 自带的预定义变量(不用定义,直接用)
#    $CI_COMMIT_BRANCH  → 当前分支名(如 "main")
#    $CI_COMMIT_SHA     → 当前提交的哈希值(如 "a1b2c3d4")
#    $CI_PROJECT_DIR    → Runner 上项目的路径
#    $CI_JOB_ID         → 当前 Job 的编号

⚠️ 重要:密码、Token 等敏感信息不要写在文件里!去 GitLab → Settings → CI/CD → Variables 添加,并勾选 Masked(隐藏显示)。


2.6 when — 控制执行时机

这个关键字控制 Job 什么时候执行。

效果什么时候用?
on_success前面都成功才执行(默认值,不写就等于这个)正常流程
manual在 GitLab 网页上点按钮才执行⭐ 部署生产环境!防止误操作
delayed等一段时间后自动执行给缓存时间,或等 DNS 生效
always不管前面成功失败都执行清理工作、发通知
on_failure前面失败了才执行出问题后自动发告警
never永远不执行配合 rules 临时禁用某个 Job
# 实战示例
deploy-to-production:
  stage: deploy
  when: manual             # ← 必须手动点按钮,防止不小心部署到生产环境
  script:
    - echo "部署到生产环境..."

cleanup:
  stage: cleanup
  when: always             # ← 不管成败都清理
  script:
    - rm -rf /tmp/build-*

notify-on-failure:
  stage: notify
  when: on_failure         # ← 只有出问题了才发通知
  script:
    - echo "构建失败了!"

2.7 rules — 精确控制"什么时候才干活"

rules 是最强大的条件控制。它按顺序检查条件,匹配到第一条就停

deploy:
  rules:
    # 条件 1:如果是 main 分支 → 执行
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: always

    # 条件 2:如果是 MR(合并请求) → 手动触发
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: manual

    # 条件 3:如果改动了 Go 文件 → 执行
    - changes:
        - "**/*.go"
        - go.mod
      when: on_success

    # 条件 4:以上都不满足 → 不执行
    - when: never

常用场景速查

# 场景 1:只在 main 分支自动部署
rules:
  - if: '$CI_COMMIT_BRANCH == "main"'

# 场景 2:只在 MR 时运行测试
rules:
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

# 场景 3:打 Tag 时触发发布
rules:
  - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'   # 匹配 v1.2.3 这样的版本号

# 场景 4:只改动了 .go 文件才触发
rules:
  - changes:
      - "**/*.go"

# 场景 5:工作日才运行(节省资源)
rules:
  - if: '$CI_PIPELINE_SOURCE == "schedule"'
  - if: '$CI_COMMIT_BRANCH == "main"'

📖 常用预定义变量

变量值举例含义
$CI_COMMIT_BRANCHmain当前分支名
$CI_COMMIT_TAGv1.0.0Tag 名(只在打 Tag 时有值)
$CI_PIPELINE_SOURCEpush / merge_request_event / schedule / web谁触发了流水线
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAMEfeature/loginMR 的来源分支
$CI_MERGE_REQUEST_TARGET_BRANCH_NAMEmainMR 要合并到的目标分支
$CI_COMMIT_MESSAGEfix: 修复登录 bug提交信息

2.8 ⚠️ 重要:分支和 Tag 不能同时判断

很多新手想写"只在 main 分支且打了 tag 时才部署",但这是做不到的——因为 $CI_COMMIT_BRANCH$CI_COMMIT_TAG 永远不会同时有值

你推送的是...$CI_COMMIT_BRANCH$CI_COMMIT_TAG
代码提交(如 main 分支)main
版本 Tag(如 v1.0.0v1.0.0

Tag 和分支是两种独立的事件,一次推送要么是提交代码(有分支名),要么是打 Tag(有 Tag 名),不可能同时发生。

那怎么办?——分开控制

实际项目中的做法是:main 分支负责 CI(编译测试),Tag 负责 CD(部署发布)

stages:
  - build
  - test
  - deploy

# main 分支推送 → 自动编译
build:
  stage: build
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  script:
    - go build -o bin/app ./cmd/server/

# main 分支推送 → 自动测试
test:
  stage: test
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  script:
    - go test ./...

# 打了版本 Tag → 才部署到生产环境
deploy:
  stage: deploy
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/'
  script:
    - echo "发布版本 $CI_COMMIT_TAG 到生产环境"

日常流程

开发阶段:
  推送代码到 main → 自动跑 build + test ✅

准备发布:
  git tag v1.0.0
  git push origin v1.0.0
  → 自动跑 deploy ✅(只跑 deploy,不跑 build/test)

💡 如果确实需要确保 Tag 一定打在 main 分支上,可以在 deploy Job 的 script 里加校验:

- git branch -r --contains $CI_COMMIT_SHA | grep "origin/main" || {
    echo "❌ 这个 tag 不在 main 分支上!"; exit 1;
  }


三、核心实战:CI 和 CD 到底怎么做?

前面学了一大堆关键字,现在我们把它们串起来,看一个真实项目是怎么用的。

3.1 CI(持续集成)— 自动编译 + 自动测试

目的:每次推送代码,自动检查"代码能不能编译通过?测试有没有挂?"

# ==============================
# 只做 CI,不做部署的配置
# ==============================
stages:
  - build          # 第一阶段:编译
  - test           # 第二阶段:测试

# ---------- Job 1: 编译 ----------
build:
  stage: build                   # 属于 build 阶段
  tags:
    - ubuntu-24.04               # 用这台机器跑
  script:
    - go build -o bin/server ./cmd/server/
  artifacts:                     # 把编译结果保存下来
    paths:
      - bin/                     # 保存 bin 文件夹
    expire_in: 1 day             # 一天后自动删除

# ---------- Job 2: 测试 ----------
test:
  stage: test                    # 属于 test 阶段(等 build 完成再跑)
  tags:
    - ubuntu-24.04
  script:
    - go test -v ./...           # -v 显示详细输出
  needs:                         # 只需要 build 完成就行
    - build

流程图

你推送代码到 GitLab
        │
        ▼
┌───────────────────┐
│  Stage 1: build    │  运行 go build,如果能编译通过 →  ✅
│  Job: build        │  生成 bin/server 文件,保存为 artifacts
└───────┬───────────┘
        │ build 成功
        ▼
┌───────────────────┐
│  Stage 2: test     │  运行 go test,测试全通过 →  ✅
│  Job: test         │  如果有测试失败 → ❌,你会收到邮件通知
└───────────────────┘

3.2 CD(持续部署)— 自动部署到服务器

目的:测试通过后,把程序自动(或手动确认后)部署到服务器。

CD 分两种方式:

方式触发方式风险适合场景
持续交付 (Continuous Delivery)测试通过后,手动点按钮才部署低 ✅生产环境
持续部署 (Continuous Deployment)测试通过后,自动部署高 ⚠️测试环境
stages:
  - build
  - test
  - deploy-staging          # 预发布环境
  - deploy-production       # 生产环境

# ... build 和 test 同上 ...

# ---------- 部署到预发布(自动) ----------
deploy-staging:
  stage: deploy-staging
  tags:
    - ubuntu-24.04
  environment:                      # 关联环境
    name: staging                   # 环境名称
    url: https://staging.example.com
  script:
    - echo "自动部署到预发布环境..."
    - ./deploy.sh staging
  only:
    - develop                       # 只在 develop 分支时自动部署

# ---------- 部署到生产环境(手动) ----------
deploy-production:
  stage: deploy-production
  tags:
    - ubuntu-24.04
  when: manual                      # ⭐ 手动触发!防止误操作
  environment:
    name: production
    url: https://example.com
  script:
    - echo "部署到生产环境(管理员已确认)..."
    - ./deploy.sh production
  only:
    - main                          # 只允许从 main 分支部署

3.3 artifacts — Job 之间如何传递文件

artifacts 就是一个 Job 的"产出物"——它做出来的东西,后面的 Job 可以直接拿来用。

类比:build Job 做了一个面包,把面包放在货架上(artifacts),test Job 从货架上取面包来检查质量(needs 或自动继承)。

build:
  stage: build
  script:
    - go build -o bin/myapp ./cmd/server/
  artifacts:
    paths:                          # 把哪些文件/文件夹放上货架
      - bin/
    name: "build-$CI_COMMIT_SHORT_SHA"  # 给制品起个名
    expire_in: 1 week               # 一周后自动清理
    when: on_success                # 只在成功时保存(默认)

deploy:
  stage: deploy
  needs: [build]                    # 告诉 GitLab:"我需要 build 的 artifacts"
  script:
    - ls bin/                       # 能看到 bin/myapp 这个文件!
    - scp bin/myapp user@server:/opt/

⚠️ 注意事项

  • 不用 needs 的话,一个 Stage 会自动接收上一个 Stage 所有 Job的 artifacts
  • needs 的话,只会接收你指定的那些 Job的 artifacts
  • artifacts 会占用存储空间,记得设 expire_in

💡 文档里到处出现的 bin/ 到底是哪里的目录?

bin/相对于项目根目录的路径,不是系统根目录的 /bin

你的项目被 GitLab Runner clone 后,在 Runner 机器上的实际位置类似:

/home/gitlab-runner/builds/随机字符串/0/你的用户名/你的项目名/
├── .gitlab-ci.yml
├── go.mod
├── cmd/server/main.go
└── bin/                    ← go build -o bin/xxx 就生成在这里
    └── demo_go

整个流程:

① go build -o bin/demo_go    → 在项目目录里生成 bin/demo_go
② artifacts: paths: [bin/]   → 把项目下的 bin/ 整个上传到 GitLab 保存
③ 下一个 Job 自动下载 artifacts → bin/ 又出现在项目目录里
④ scp bin/demo_go 目标服务器    → 从项目目录传走

bin/ 这个名字可以随便改——比如叫 output/dist/,只是一个装了编译产物的普通文件夹。


3.4 environment — 在 GitLab 里管理部署历史

用了 environment 后,GitLab 网页上会多出一个"环境"页面,显示:

  • 📜 每次部署的历史记录
  • 🔍 当前部署的是哪个 commit
  • ⏪ 一键回滚按钮
  • 🟢/🔴 环境是否正常运行
deploy:
  stage: deploy
  environment:
    name: production                  # 环境名称(必填)
    url: https://myapp.example.com    # 访问地址(选填,但建议填)
    on_stop: stop-production          # 关联一个"销毁环境"的 Job

# 停止/销毁环境的 Job(手动触发)
stop-production:
  stage: deploy
  environment:
    name: production
    action: stop                      # 表示这是"停止环境"的操作
  when: manual
  script:
    - echo "关闭生产环境..."

3.5 needs — 谁说一定要按顺序来?

默认规则是必须一个 Stage 完成才开始下一个。needs 让你打破这个规则

stages:
  - build
  - test
  - deploy

build:
  stage: build
  # ... 编译要 5 分钟 ...

fast-test:              # 简单测试,不需要编译产物
  stage: test
  needs: []             # ← 空的!不等 build,立即开始!
  script:
    - echo "只检查代码风格,不需要编译"
    - golangci-lint run

slow-test:              # 集成测试,需要编译产物
  stage: test
  needs: [build]        # ← 等 build 完成
  script:
    - ./bin/server &
    - curl http://localhost:8080/

quick-deploy:           # 部署到测试环境
  stage: deploy
  needs: [build]        # ← 只等 build,不等任何 test!
  script:
    - echo "快速部署到测试环境"

效果:快速测试 + 编译 + 快速部署 可以同时进行,大大缩短等待时间!


四、部署到远程服务器:Runner 做好的程序怎么送到线上?

前面讲了 CD 的概念、environment 关键字,但最终要解决一个问题:

Runner 上编译好的二进制文件,怎么传到目标服务器跑起来

整体流程:

Runner 服务器                        目标服务器 (测试/正式)
┌──────────────┐                    ┌──────────────┐
│  编译 Go      │                    │              │
│  生成二进制    │  ── 网络传输 ──→   │  接收文件     │
│              │                    │  重启服务     │
│  (CI 完成)   │                    │  (CD 完成)   │
└──────────────┘                    └──────────────┘

4.1 前置准备:打通 Runner 到目标服务器的 SSH

不管用哪种传输方式,第一步都是让 Runner 能免密码 SSH到目标服务器。

① 在 Runner 机器上生成密钥
# 用 gitlab-runner 用户生成 SSH 密钥
sudo -u gitlab-runner ssh-keygen -t ed25519 -C "gitlab-runner" -f /home/gitlab-runner/.ssh/id_ed25519 -N ""

# 查看公钥(马上要用)
sudo cat /home/gitlab-runner/.ssh/id_ed25519.pub
② 把公钥放到目标服务器

目标服务器上执行:

# 把 Runner 的公钥追加到 authorized_keys
echo "ssh-ed25519 AAAAC3... gitlab-runner" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
③ 测试连通 + 消除指纹确认
# 在 Runner 上测试
sudo -u gitlab-runner ssh-keyscan 目标服务器IP >> /home/gitlab-runner/.ssh/known_hosts
sudo -u gitlab-runner ssh 用户名@目标服务器IP "echo 连接成功"

4.2 方式一:scp + ssh(最简单,推荐新人用)

直接把一个文件拷过去,然后远程执行重启命令。适合只部署单个二进制文件的场景。

# 在 GitLab CI/CD Variables 中配置以下变量:
#   STAGING_HOST:  测试服务器 IP
#   STAGING_USER:  测试服务器用户名
#   PROD_HOST:     正式服务器 IP
#   PROD_USER:     正式服务器用户名

deploy-staging:
  stage: deploy
  tags:
    - ubuntu-24.04
  environment:
    name: staging
    url: http://${STAGING_HOST}:8080
  script:
    # ① 拷贝文件
    - scp ${APP_NAME} ${STAGING_USER}@${STAGING_HOST}:/opt/${APP_NAME}/
    # ② 远程重启服务
    - ssh ${STAGING_USER}@${STAGING_HOST} "sudo systemctl restart ${APP_NAME}"
    # ③ 等 2 秒检查服务是否正常
    - sleep 2
    - ssh ${STAGING_USER}@${STAGING_HOST} "systemctl is-active ${APP_NAME}"

💡 关键点:变量 STAGING_HOSTPROD_HOST 等建议在 GitLab 网页 Settings → CI/CD → Variables 中设置,不要硬编码在文件里。


4.3 方式二:rsync 增量同步(适合多文件、大项目)

如果需要同步的不仅是二进制文件,还有配置文件、静态页面等,用 rsync 只传有变化的部分,比 scp 快很多。

deploy:
  stage: deploy
  script:
    # -a 归档模式  -v 显示详情  -z 压缩  --delete 删除目标多余文件
    - rsync -avz --delete \
        ${APP_NAME} \
        config/ \
        static/ \
        ${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/
    - ssh ${PROD_USER}@${PROD_HOST} "sudo systemctl restart ${APP_NAME}"

4.4 方式三:Docker 镜像部署(更标准、更隔离)

如果目标服务器装了 Docker,可以把程序打包成镜像,推送过去运行。好处是环境完全一致,不会出现"我这能跑你那不行"。

docker-build-push:
  stage: build
  script:
    - docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} .
    - docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
    - docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}

deploy-docker:
  stage: deploy
  script:
    - ssh ${PROD_USER}@${PROD_HOST} "
        docker pull ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA} &&
        docker stop ${APP_NAME} || true &&
        docker rm ${APP_NAME} || true &&
        docker run -d --name ${APP_NAME} -p 8080:8080 ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
      "

4.5 目标服务器上如何管理服务?(systemd)

文件传过去后,需要一个"管家"来保证程序一直在后台运行、崩溃自动重启。Linux 上最常用的是 systemd

在目标服务器上创建服务文件 /etc/systemd/system/demo_go.service

[Unit]
Description=demo_go HTTP Server        # 服务描述
After=network.target                   # 网络就绪后再启动

[Service]
Type=simple
User=deploy                            # 用哪个用户运行(不要用 root)
WorkingDirectory=/opt/demo_go
ExecStart=/opt/demo_go/demo_go         # 二进制文件的路径
Restart=always                         # 崩溃了自动重启
RestartSec=5                           # 等 5 秒再重启
Environment="PORT=8080"                # 环境变量

[Install]
WantedBy=multi-user.target

常用管理命令:

sudo systemctl daemon-reload     # 修改配置文件后重新加载
sudo systemctl enable demo_go    # 开机自启
sudo systemctl start demo_go     # 启动
sudo systemctl stop demo_go      # 停止
sudo systemctl restart demo_go   # 重启
sudo systemctl status demo_go    # 查看状态和日志

4.6 三种部署方式对比

方式难度适用场景优点缺点
scp + ssh⭐ 简单单文件、小项目配置最少,开箱即用每次传全量文件
rsync + ssh⭐⭐ 一般多文件、静态资源增量同步,速度快需要额外安装 rsync
Docker 镜像⭐⭐⭐ 较难微服务、多环境环境完全隔离需要 Docker 环境

对于你当前的 demo_go 项目,scp + ssh + systemd 完全够用。


五、版本回退:部署出问题了怎么办?

部署不是一锤子买卖,万一新版本有 bug,必须能快速回退到上一个正常版本

回退的核心思路就四个字:先备后换


5.1 最简单的方式:保留旧文件的备份

每次部署时,先把旧文件改个名,再放新的。出问题了就把备份改回来。

deploy:
  stage: deploy
  tags:
    - ubuntu-24.04
  environment:
    name: production
  script:
    # ① 远程备份旧版本(加上时间戳)
    - ssh ${PROD_USER}@${PROD_HOST} "
        if [ -f /opt/${APP_NAME}/${APP_NAME} ]; then
          cp /opt/${APP_NAME}/${APP_NAME} /opt/${APP_NAME}/${APP_NAME}.bak.$(date +%Y%m%d_%H%M%S);
        fi
      "
    # ② 传新版本
    - scp ${APP_NAME} ${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/
    # ③ 重启服务,如果失败就自动回滚
    - |
      ssh ${PROD_USER}@${PROD_HOST} "sudo systemctl restart ${APP_NAME}"
      if [ $? -ne 0 ]; then
        echo "❌ 部署失败!自动回滚..."
        LATEST_BAK=$(ssh ${PROD_USER}@${PROD_HOST} "ls -t /opt/${APP_NAME}/${APP_NAME}.bak.* | head -1")
        ssh ${PROD_USER}@${PROD_HOST} "cp ${LATEST_BAK} /opt/${APP_NAME}/${APP_NAME} && sudo systemctl restart ${APP_NAME}"
        exit 1
      fi
    - echo "✅ 部署成功"

5.2 推荐方式:用 GitLab 的 Rollback 按钮

GitLab 的 environment 功能自带一键回滚——直接用上一次成功的产物重新部署。

deploy:
  stage: deploy
  tags:
    - ubuntu-24.04
  environment:
    name: production
    # 关键:指定回滚时跑哪个 Job
    # GitLab 会自动用上一次 deploy Job 的 artifacts 重新执行
  script:
    # 部署前先备份
    - ssh ${PROD_USER}@${PROD_HOST} "
        [ -f /opt/${APP_NAME}/${APP_NAME} ] && cp /opt/${APP_NAME}/${APP_NAME} /opt/${APP_NAME}/${APP_NAME}.bak || true
      "
    - scp ${APP_NAME} ${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/
    - ssh ${PROD_USER}@${PROD_HOST} "sudo systemctl restart ${APP_NAME}"

在 GitLab 网页上回滚的路径:

你的项目 → Deployments → Environments → production
  → 点击要回滚到的那个版本 → 点击 "Rollback environment" 按钮

GitLab 会自动:

  1. 找到那次部署用的 artifacts(编译产物)
  2. 用那些 artifacts 重新跑 deploy Job
  3. 完成回滚

⚠️ 前提:该版本的 artifacts 还没过期(受 expire_in 控制)


5.3 手动回滚 Job(兜底方案)

单独写一个回滚 Job,出问题时手动点击执行:

rollback:
  stage: deploy
  tags:
    - ubuntu-24.04
  when: manual                          # 手动触发
  environment:
    name: production
    action: rollback                    # 标记为回滚操作
  script:
    # 找到最新的备份文件
    - LATEST_BAK=$(ssh ${PROD_USER}@${PROD_HOST} "ls -t /opt/${APP_NAME}/${APP_NAME}.bak.* 2>/dev/null | head -1")
    - |
      if [ -z "${LATEST_BAK}" ]; then
        echo "❌ 没有可用的备份文件!"
        exit 1
      fi
    - echo "回滚到备份: ${LATEST_BAK}"
    - ssh ${PROD_USER}@${PROD_HOST} "
        cp ${LATEST_BAK} /opt/${APP_NAME}/${APP_NAME} &&
        sudo systemctl restart ${APP_NAME}
      "
    - echo "✅ 回滚完成"

5.4 进阶:多版本保留策略

保留最近 N 个版本,方便选择回退到任意版本:

deploy:
  stage: deploy
  script:
    # ① 把新文件命名为带版本号的文件
    - scp ${APP_NAME} ${PROD_USER}@${PROD_HOST}:/opt/${APP_NAME}/releases/${CI_COMMIT_SHORT_SHA}/
    # ② 更新一个软链接指向当前版本
    - ssh ${PROD_USER}@${PROD_HOST} "
        ln -sfn /opt/${APP_NAME}/releases/${CI_COMMIT_SHORT_SHA}/${APP_NAME} /opt/${APP_NAME}/current &&
        sudo systemctl restart ${APP_NAME}
      "
    # ③ 只保留最近 5 个版本,旧的删掉
    - ssh ${PROD_USER}@${PROD_HOST} "
        cd /opt/${APP_NAME}/releases && ls -t | tail -n +6 | xargs -r rm -rf
      "

目录结构:

/opt/demo_go/
├── current → releases/a1b2c3d/demo_go   # 软链接,指向当前版本
└── releases/
    ├── a1b2c3d/demo_go                   # 版本 1
    ├── e4f5g6h/demo_go                   # 版本 2
    └── i7j8k9l/demo_go                   # 版本 3(最新)

回退时只需要改软链接:

# 回退到版本 e4f5g6h
ln -sfn /opt/demo_go/releases/e4f5g6h/demo_go /opt/demo_go/current
systemctl restart demo_go

5.5 回退策略对比

方式实现难度回退速度GitLab UI 支持适合场景
备份旧文件⭐ 简单应急兜底
GitLab Rollback 按钮⭐⭐ 一般快(需重新跑 Job)✅ 原生支持常规使用
手动回滚 Job⭐⭐ 一般半支持备份兜底
多版本软链接⭐⭐⭐ 较难秒级需要秒级回退的大项目

💡 建议组合使用:GitLab Rollback(主力) + 备份文件回滚(兜底)



六、你的 demo_go 项目 — 完整 CI/CD 配置

针对你当前的 Go HTTP 服务,这是一个可以直接用的配置文件:

# ============================================
# .gitlab-ci.yml — demo_go 项目
# 功能:编译 → 测试 → 手动部署
# ============================================

# 第一步:定义流水线有哪些阶段(按顺序执行)
stages:
  - build
  - test
  - deploy

# 第二步:定义全局变量(所有 Job 都能用)
variables:
  GO_VERSION: "1.25"
  APP_NAME: "demo_go"

# ==================== 构建 ====================
build:
  stage: build                        # 属于"构建"阶段
  tags:
    - ubuntu-24.04                    # 用贴了这个标签的 Runner
  script:
    # -ldflags="-s -w" 可以减小编译出来的文件体积
    - go build -ldflags="-s -w" -o bin/${APP_NAME} ./cmd/server/
    - echo "构建完成!文件大小:"
    - ls -lh bin/${APP_NAME}
  artifacts:                          # 编译产物保存下来
    paths:
      - bin/
    expire_in: 1 day                  # 一天后自动删除

# ==================== 测试 ====================
test:
  stage: test                         # 属于"测试"阶段(等 build 完成)
  tags:
    - ubuntu-24.04
  script:
    # -race 检测并发问题,-coverprofile 生成覆盖率报告
    - go test -v -race -coverprofile=coverage.out ./...
    # 打印每个函数的测试覆盖率
    - go tool cover -func=coverage.out
  coverage: '/total:.*?(\d+\.\d+)%/'   # GitLab 自动提取覆盖率数字
  artifacts:
    reports:
      coverage_report:                # 让 GitLab 解析覆盖率
        coverage_format: cobertura
        path: coverage.out

# ==================== 部署(手动触发) ====================
deploy:
  stage: deploy
  tags:
    - ubuntu-24.04
  when: manual                         # ⭐ 必须手动点按钮才部署
  environment:
    name: production
  script:
    - echo "正在部署 ${APP_NAME} 到生产环境..."
    - sudo cp bin/${APP_NAME} /opt/${APP_NAME}/
    - sudo systemctl restart ${APP_NAME}
    - echo "部署完成!"
  only:
    - main                             # 只允许从 main 分支部署

七、从零到一:第一次跑流水线的完整步骤

第 1 步:写好 .gitlab-ci.yml

把上面的文件内容放到你项目的根目录,文件名必须是 .gitlab-ci.yml

第 2 步:推送到 GitLab

git add .gitlab-ci.yml
git commit -m "添加 CI/CD 配置"
git push origin main

第 3 步:去 GitLab 网页上看结果

  1. 打开你的 GitLab 项目页面
  2. 点击左侧菜单:CI/CD → Pipelines
  3. 你会看到一条新的流水线在运行 —— 有绿色 ✅ 就是成功,红色 ❌ 就是失败
  4. 点击流水线,再点击某个 Job,能看到实时日志
GitLab 页面路径:
  你的项目 → CI/CD → Pipelines → 点击某条流水线 → 点击某个 Job → 看日志

第 4 步:手动触发部署

如果 configure 里有 when: manual 的 Job:

  1. 在流水线页面,你会看到一个播放按钮 ▶️(而不是自动执行)
  2. 点击它,部署就开始执行了

八、Runner 那些事

什么是 Runner?

Runner 就是真正干活的那台机器。GitLab 本身只负责调度,不负责执行——它把任务派给 Runner,Runner 干完活把结果回报给 GitLab。

你的代码 → GitLab(大脑,负责指挥)
                │
                ▼
           Runner(手脚,负责干活)
                │
                ▼
         执行 go build、go test...

你项目用的 Shell Executor

你的 Runner 用的是 Shell Executor,意味着 Job 直接在 Runner 机器的 shell 里执行。

# 在 Runner 机器上,可以用这些命令管理
gitlab-runner status       # 看 Runner 是否在运行
gitlab-runner list         # 列出已注册的 Runner
gitlab-runner verify       # 检查 Runner 连接是否正常
cat /etc/gitlab-runner/config.toml   # 查看 Runner 配置

常见坑 ⚠️

根据你之前的排查经验:

问题现象解决
.bash_logout 干扰Job 报 prepare environment: exit status 1清空 /home/gitlab-runner/.bash_logout
缺少依赖go: command not found在 Runner 机器上装好 Go
缺少 gitclone 代码失败apt-get install -y git
权限不足Permission denied检查 gitlab-runner 用户的权限

九、常见场景速查(拿来就用)

场景 1:只在 MR(合并请求)时跑测试

test:
  stage: test
  script:
    - go test ./...
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

场景 2:打 Tag 时自动发布

release:
  stage: deploy
  script:
    - echo "发布版本 $CI_COMMIT_TAG"
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'

场景 3:定时任务(每天晚上跑)

不需要改 .gitlab-ci.yml!去 GitLab:CI/CD → Schedules → New schedule,设置每天凌晨 2 点跑。

场景 4:多个 Go 版本同时测试

test-go-1.24:
  stage: test
  image: golang:1.24
  script:
    - go test ./...

test-go-1.25:
  stage: test
  image: golang:1.25
  script:
    - go test ./...

场景 5:部署失败自动回滚

deploy:
  stage: deploy
  script:
    - ./deploy.sh
  environment:
    name: production
    on_stop: rollback       # 关联回滚 Job

rollback:
  stage: deploy
  when: manual
  environment:
    name: production
    action: stop
  script:
    - echo "回滚到上一个版本..."

十、快速参考卡片

┌──────────────────────────────────────────────────────────┐
                  .gitlab-ci.yml 大局观                     
├──────────────────────────────────────────────────────────┤
                                                          
  stages: [...]           全局:定义阶段顺序               
  variables: {...}        全局:定义变量                  
  default: {...}          全局:默认配置                  
                                                          
  ┌─────────────────────────────────────────────────┐     
    my-job-1:           自定义 Job                      
      stage: test       在哪个阶段                       
      tags: [linux]     哪个 Runner                   
      script: [...]     干什么活(必填!)                
      artifacts: {...}  产出什么                         
      environment: {}   部署到哪                         
      rules: [...]      什么时候触发                     
      when: manual      自动还是手动                     
      needs: [job-x]    等谁完成                         
  └─────────────────────────────────────────────────┘     
                                                          
  ┌─────────────────────────────────────────────────┐     
    my-job-2:           另一个 Job                       
      ...                                                
  └─────────────────────────────────────────────────┘     
└──────────────────────────────────────────────────────────┘

我想做... → 用什么关键字

我想...关键字
控制执行顺序stages + stage + needs
指定哪台机器跑tags
只在特定分支运行rules + $CI_COMMIT_BRANCH
手动点了才执行when: manual
把构建结果传给下一步artifacts
部署后能在网页看到历史environment
密码/Token 不写在文件里GitLab UI → Settings → CI/CD → Variables
下载依赖更快cache
失败了自动重试retry
某个 Job 失败不影响整体allow_failure: true
定时自动跑GitLab UI → CI/CD → Schedules(无需改文件)
配置文件太大拆分include

十一、下一步可以做什么?

  1. 动手试试:把第六章的完整配置复制到你的 .gitlab-ci.yml,推送看效果
  2. 装一个 Runner:如果还没有 Runner,在自己的服务器上装 gitlab-runner 并注册
  3. 设置变量:把服务器的 SSH 密钥、部署密码等放到 GitLab Variables 中
  4. 加上通知:在 after_script 里加企业微信/钉钉/Slack 通知
  5. 加上代码检查:增加 golangci-lint Job,自动检查代码质量