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

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

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

用快递公司来类比 🏭

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

复制代码
客户下单 → 仓库打包 → 质检 → 快递发货

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

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

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

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


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

想象一个汽车制造流水线

yaml 复制代码
                 ┌──────────────────────────────────────┐
  代码推送 ──→   │  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 的对应关系:

yaml 复制代码
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 格式,看懂以下三点就够了:

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 名称

来看看具体是什么意思:

yaml 复制代码
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 命令。你可以把平时在终端里敲的命令直接写进去。

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

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

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

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

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

yaml 复制代码
# ✅ 写法一:用 && 串起来
script:
  - cd /tmp && ls

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

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


2.3 tags --- 派哪个 Runner 干活?

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

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

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

yaml 复制代码
build:
  tags:
    - ubuntu-24.04   # GitLab 会去找有 ubuntu-24.04 标签的 Runner

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


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

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

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

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

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

执行规则

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

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


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

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

yaml 复制代码
# ① 你自己定义的变量
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
yaml 复制代码
# 实战示例
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 是最强大的条件控制。它按顺序检查条件,匹配到第一条就停

yaml 复制代码
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

常用场景速查

yaml 复制代码
# 场景 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_BRANCH main 当前分支名
$CI_COMMIT_TAG v1.0.0 Tag 名(只在打 Tag 时有值)
$CI_PIPELINE_SOURCE push / merge_request_event / schedule / web 谁触发了流水线
$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME feature/login MR 的来源分支
$CI_MERGE_REQUEST_TARGET_BRANCH_NAME main MR 要合并到的目标分支
$CI_COMMIT_MESSAGE fix: 修复登录 bug 提交信息

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

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

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

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

那怎么办?------分开控制

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

yaml 复制代码
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 到生产环境"

日常流程

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

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

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

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


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

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

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

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

yaml 复制代码
# ==============================
# 只做 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

流程图

bash 复制代码
你推送代码到 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) 测试通过后,自动部署 高 ⚠️ 测试环境
yaml 复制代码
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 或自动继承)。

yaml 复制代码
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 机器上的实际位置类似:

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

整个流程:

python 复制代码
① 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
  • ⏪ 一键回滚按钮
  • 🟢/🔴 环境是否正常运行
yaml 复制代码
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 让你打破这个规则

yaml 复制代码
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 上编译好的二进制文件,怎么传到目标服务器跑起来

整体流程:

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

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

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

① 在 Runner 机器上生成密钥
bash 复制代码
# 用 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
② 把公钥放到目标服务器

目标服务器上执行:

bash 复制代码
# 把 Runner 的公钥追加到 authorized_keys
echo "ssh-ed25519 AAAAC3... gitlab-runner" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
③ 测试连通 + 消除指纹确认
bash 复制代码
# 在 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(最简单,推荐新人用)

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

yaml 复制代码
# 在 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 快很多。

yaml 复制代码
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,可以把程序打包成镜像,推送过去运行。好处是环境完全一致,不会出现"我这能跑你那不行"。

yaml 复制代码
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

ini 复制代码
[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

常用管理命令:

bash 复制代码
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 最简单的方式:保留旧文件的备份

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

yaml 复制代码
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 功能自带一键回滚------直接用上一次成功的产物重新部署。

yaml 复制代码
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 网页上回滚的路径:

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

GitLab 会自动:

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

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


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

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

yaml 复制代码
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 个版本,方便选择回退到任意版本:

yaml 复制代码
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
      "

目录结构:

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

回退时只需要改软链接:

bash 复制代码
# 回退到版本 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 服务,这是一个可以直接用的配置文件:

yaml 复制代码
# ============================================
# .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

bash 复制代码
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。

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

你项目用的 Shell Executor

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

bash 复制代码
# 在 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
缺少 git clone 代码失败 apt-get install -y git
权限不足 Permission denied 检查 gitlab-runner 用户的权限

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

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

yaml 复制代码
test:
  stage: test
  script:
    - go test ./...
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

场景 2:打 Tag 时自动发布

yaml 复制代码
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 版本同时测试

yaml 复制代码
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:部署失败自动回滚

yaml 复制代码
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 "回滚到上一个版本..."

十、快速参考卡片

yaml 复制代码
┌──────────────────────────────────────────────────────────┐
│                  .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,自动检查代码质量
相关推荐
易生一世2 小时前
GitHub Copilot概述
github
暴雨课堂2 小时前
宝塔和云效webhook配置
github
zahuilg102 小时前
Mac原生终端SSH一键快捷连接|无需装软件、极简安装、快速上手
macos·ssh·github·终端
用户1712819473753 小时前
autoflake:Python 代码里没用的 import,让它自己清掉
github
南知意-3 小时前
MonkeyCode:长亭开源的企业级AI开发平台,GitHub 3.2k Star!
人工智能·ai·开源·github·ai编程·开源项目
humpy28873 小时前
测试用记录
github
uhakadotcom3 小时前
在 Python 开发中 transitions 的使用
后端·面试·github
大刚测试开发实战3 小时前
TestHub重磅更新!AI用例生成增加流式输出、Markdown文档上传、模型配置检测、AI评审开关控制...
vue.js·后端·github
阿里嘎多学长4 小时前
2026-06-09 GitHub 热点项目精选
开发语言·程序员·github·代码托管