01 / THE PROBLEM

这套系统解决什么问题

这一章回答第一个疑问:我们到底在解决什么麻烦?为什么小团队也需要一套「系统」?

想象一下没有这套系统时的日常:需求记在聊天软件里,聊着聊着就沉底了;代码在各自电脑上,靠 U 盘和口头同步;测试靠人肉点一遍,点漏了就上线即事故;部署靠「我记得那台服务器上好像是这么改的」;crontab(定时任务)改完就忘,三个月没人知道某台机器凌晨 3 点在跑什么。

这套系统要解决的,就是把这一切从人脑记忆里搬出来,变成代码和自动化。源文档把需求拆成了 7 块,外加 1 条贯穿性需求,用大白话说就是:

#需求(大白话版)用什么解决
1需求、Bug 得有地方提、有人跟、有状态可查GitLab CE 自带 Issues
2代码要有版本管理,谁改了什么一目了然GitLab CE 自带 Git 仓库
3每次改代码自动跑测试,别等上线才发现问题GitLab CI
4测试、构建这些重复劳动全部自动化(CI,持续集成)GitLab CI + Runner
5点一下按钮就能把新版本部署到服务器(CD,持续部署)GitLab CI + Ansible(SSH 推送)
6各台服务器上的服务、定时任务、脚本要管得起来Ansible(代码化管理,存 Git)
7密码、密钥不能明文裸奔,也不能记不住SOPS + age + GitLab CI 变量
8AI agent 参与干活,且操作要统一走固定流程自建 skill 库(操作手册 + 模板 + 脚本)

注意第 8 条:这个团队里有一位特殊成员——运行在你个人工作电脑上的 AI coding agent(可以理解为一位会读写代码、会执行命令的 AI 同事)。整套系统的每一个设计决策,都会考虑到「人和 AI 都能用」:一切配置都是文本文件、一切操作都有固定流程,因为 AI 擅长读写文本、按手册执行,不擅长点网页、靠感觉办事。

02 / BIG PICTURE

30 秒看懂全貌

这一章解决「东西太多记不住」的问题:先用六句话记住骨架,再学会认颜色——后文所有图和标记都用同一套颜色。

源文档的最后一页有个「一页纸总结」,它就是整套系统的骨架:

学会读颜色

本文给系统里的每类「角色」分配了一个固定颜色,架构图、数据流图、正文标记全部通用。读完你会自然形成条件反射:

红色 = GitLab 平台(系统中心) 蓝色 = CI / Runner / 流水线 黄色 = Ansible / 运维自动化 紫色 = 密钥 / SOPS 青绿 = SSH 通道 / 网络链路 灰色 = 中性角色(你的电脑、仓库等)

一句话概括这套系统的工作方式:所有事情从办公网一侧发起,经 SSH 推到目标机器;所有事实写在 Git 仓库里;所有重复劳动交给流水线和 AI agent。后面的每一章,都是对这句话某一个部分的展开。

03 / MACHINES & NETWORK

需要几台什么样的机器

这一章回答「我要准备什么硬件」:先给一张采购/盘点清单,再用一张可点击的拓扑图看懂它们怎么连。

机器清单(照单盘点即可)

机器配置要求在哪个网干什么 / 不干什么
GitLab 服务器 建议 4 核 8G 内存、100G 磁盘;最低 2 核 4G。装好 Docker 办公网 GitLab CE(Docker 容器)+ Runner(与 GitLab 同机)+ 容器镜像仓库
你的工作电脑 日常开发机即可 办公网 你本人 + AI agent + skill 库都在这台机器上;装好 Ansible、sops、glab CLI,按 5.2 节配好 SSH
文件服务器 保持现状,不加任何负担 双网卡:同时接办公网和纯内网 只做跳板/中转:在两网之间转发 SSH 的 TCP 流量。不在上面跑构建和部署——它是共享基础设施,稳定性优先
办公网生产机(若干) 按业务需要 办公网 零 agent:什么都不用装,只要 SSH 可达;建一个权限最小的 deploy 部署账号
纯内网生产机(若干) 按业务需要 纯内网(无公网) 零 agent、零出网需求:只要能被文件服务器 SSH 到即可,连 GitLab 都不需要知道

一句话:新增的只有一台 4C8G 的办公网机器,其余全是现有机器的角色明确化。

图 1 · 网络拓扑(点击组件查看角色说明,点击空白复位)
办公网(可上公网) 纯内网(无公网) SSH SSH SSH SSH SSH(ProxyJump 转发) 你的工作电脑 你本人 · AI agent skill 库 · Ansible · sops GitLab 服务器 GitLab CE · 镜像仓库 · CI 变量 Runner(同机) 办公网生产机(若干) 零 agent,SSH 直连可达 由 Ansible 管理 文件服务器 双网卡 · 只做跳板/中转 纯内网生产机(若干) 零 agent · 零出网需求
青绿线 = SSH 通道。注意纯内网大框里没有任何指向外部的箭头——内网机器从不主动发起连接。

GitLab 服务器(系统中心)

整套系统的心脏:Issue、Git 仓库、MR 评审、CI/CD 调度、容器镜像仓库、CI 变量全在它身上。建议 4 核 8G、100G 磁盘,放在办公网。Runner 和它同机,流水线的测试、构建、部署任务都在这台机器的一次性 Docker 容器里执行。

GitLab 平台 CI / Runner Ansible 管理的机器 SSH 通道 / 跳板 你的工作环境

GitLab 为什么放办公网?

候选位置有三个,源文档逐一分析过:

候选位置优点缺点结论
办公网专用小服务器/虚拟机 你和队友随时可访问;GitLab 自身能连公网(拉镜像、发通知);Runner 到办公网生产机直连 需要一台 4C8G 的机器 首选
文件服务器上 不用找新机器;双网卡,理论上两边都能访问 文件服务器是共享基础设施,GitLab 吃内存,一旦出问题影响面大 资源充足时的备选
你的工作电脑(Docker) 零成本 你关机/休假队友就用不了;电脑重装系统风险大 仅适合过渡期

为什么不能放纯内网?因为纯内网里的 GitLab 无法访问公网,装依赖、拉基础镜像、更新系统都极痛苦;而且你在办公网访问它还要绕跳板,日常体验差。

这里藏着全篇最重要的一个洞察:内网机器从头到尾都不需要主动访问 GitLab。想通这一点,就引出了下一章的核心决策。

04 / THE KEY DECISION

最重要的一个决策:推送而不是拉取

这一章讲透整套系统最有价值的设计选择——为什么内网机器上可以什么都不装。理解了它,后面所有工具的选择都顺理成章。

两种部署模型

「把代码部署到服务器」这件事,业界有两种经典做法:

拉取模型(pull)推送模型(push)
怎么工作 每台目标服务器上装一个 agent(如 GitLab Runner、ArgoCD、Salt minion),agent 主动连中心服务器领任务、拉制品 中心侧(CI Runner 或你的电脑)通过 SSH 主动连到目标服务器,把东西推过去、把命令执行掉
网络要求 目标机必须能连到中心 只要求中心能 SSH 到目标
在你们网络里的下场 纯内网机器连不到办公网的 GitLab,硬用就得在内网再搭一套 GitLab/Runner 并做两边同步——复杂度直接翻倍 天然存在「办公网 → 文件服务器 → 内网」这条 SSH 链路,直接用

你们的网络里恰好存在这样一条链路:

办公网(GitLab Runner / 你的电脑)──SSH──► 文件服务器 ──SSH──► 纯内网机器

SSH 原生支持多级跳板(ProxyJump,下面详解),Ansible 也是纯 SSH 推送。所以整套系统的控制流和数据流统一为:

从办公网一侧发起,经 SSH 推送。纯内网机器上不需要安装任何常驻 agent,不需要任何特殊网络配置,只要能被文件服务器 SSH 到即可。

这就是「内网机器零 agent」的全部秘密——不是用了什么黑科技,而是选了一个贴合你们网络条件的模型。

ProxyJump:一次 SSH 穿越跳板的三步原理

从 Runner(或你的电脑)到纯内网机器,中间隔着文件服务器。SSH 的 -J / ProxyJump 选项能把多级跳转压缩成一条命令:

一条命令直达内网机器
ssh -J ops@<文件服务器IP> deploy@<内网机器IP>

很多人在这里有误解,以为「跳板机替我登录了内网机器」。实际上它的工作原理分三步,务必看清:

  1. 你的 SSH 客户端先和文件服务器建立连接,用你的私钥认证(文件服务器上存你的公钥);
  2. 文件服务器只做一件事:在它和内网机器之间转发 TCP 流量——相当于一根网线;
  3. 你的客户端再通过这根「网线」和内网机器做认证——还是你的私钥对内网机器上你的公钥。认证是端到端加密的,文件服务器看不到也解不开内容。

结论非常关键:文件服务器上不需要存任何到内网机器的私钥,它只要网络可达内网机器即可。即使文件服务器被入侵,攻击者拿到的也只是「能向内网机器发起 TCP 连接」的能力,没有钥匙。这比「先 ssh 到文件服务器、再在文件服务器上存一把私钥 ssh 到内网」安全得多。

把跳转关系写进配置,以后无感

~/.ssh/config(Runner 和你的电脑通用)
# 跳板:文件服务器
Host fileserver
  HostName <文件服务器IP>
  User ops                      # 文件服务器上专门开的跳板账号,权限最小化
  IdentityFile ~/.ssh/id_ed25519

# 办公网生产机(直连)
Host office-*
  User deploy
  IdentityFile ~/.ssh/id_ed25519

# 纯内网生产机(自动走跳板)
Host intranet-*
  User deploy
  ProxyJump fileserver
  IdentityFile ~/.ssh/id_ed25519

这段在干什么:给所有服务器起统一的主机名前缀(office-xxx / intranet-xxx),用通配符各写一条规则。以后 ssh intranet-app1 一条命令直连内网机器,跳板自动进行。

deploy 是目标机上的部署专用账号,只给部署需要的权限(写应用目录、重启自己的服务),不用 root。你的电脑和 Runner 各持一对独立密钥对,公钥登记到目标机的 deploy 账号和文件服务器的 ops 账号上——钥匙按「人/系统」分开,哪把泄露了单独吊销哪把

05 / TOOLCHAIN

技术栈逐个讲

这一章把每个工具单独讲透:它是什么、本身能做什么、在这套系统里承担哪几项功能、为什么选它。顺序按「从代码到服务器」的链路排列。

Git

版本管理的地基

它是什么

Git 是代码的「时光机」:每一次修改都存成一个快照(commit,提交),谁改的、改了什么、为什么改,全部可查可回退。它是这一切的地基——后面说的「事实来源」「改动可审计」,全靠它。

在这套系统里干什么

  • 所有项目代码的版本管理;分支策略用主干开发的简化版main 是保护分支(永远可发布),每个 Issue 开一个短分支,命名 类型/Issue号-简述,如 fix/128-null-pointer
  • 版本发布 = 打 git tag(如 v1.4.0),tag 同时触发 CD 流水线;
  • ops 仓库、skill 库也都用 Git 管理——Git 历史就是「谁什么时候改了什么」的审计链。

GitLab CE

一个中心 · 系统的红色心脏

它是什么

把 GitLab 想成「你们团队的私有小 GitHub + 项目管家 + 机器人调度中心」三合一。CE 是完整开源的社区版,下面列的功能一个都不少,不需要任何付费许可。

在这套系统里干什么(逐项)

  • Issues:需求与 Bug 的唯一入口。支持 Markdown 描述、标签(bug/feature/chore/ops + P0/P1/P2)、指派人、里程碑、看板(Open → Doing → Review → Done),并与分支/MR 双向关联。Issue 模板(.gitlab/issue_templates/)统一「报 bug 必须带复现步骤/期望行为/实际行为」——这对 AI agent 尤其重要,agent 的工作质量直接取决于 Issue 描述的质量
  • Git 仓库:代码托管 + 版本管理,版本用 git tag + GitLab Release 管理;
  • MR(合并请求)评审:一切改动走 MR,规则是「流水线全绿 + 至少 1 人批准」才能合并——AI agent 开的 MR 同样要人批准,这是质量闸门;
  • CI 调度:决定每条流水线「什么时候跑、跑哪些任务」,结果显示在 MR 页面上,红的合不进;
  • 容器镜像仓库:自带的 Docker 镜像仓库开箱即用(当前制品用 tar 包,未来办公网服务容器化时再启用);
  • CI 变量:存流水线用的密钥(如 age 私钥),勾选 Masked(日志打码)+ Protected(只有保护分支的流水线能读到),不进 Git。

为什么是它

小团队,「少维护一套系统、少学一套概念」比「省内存」更重要。Issue、代码、MR、CI 在同一个系统里,Issue 可以直接关联分支和提交,不用维护第二套工具。一台 4C8G 的机器就跑得很舒服。

/opt/gitlab/docker-compose.yml
services:
  gitlab:
    image: gitlab/gitlab-ce:latest
    container_name: gitlab
    hostname: gitlab.<公司内网域名或直接用IP>
    restart: always
    ports:
      - "8080:80"      # Web 界面:浏览器访问 http://<服务器IP>:8080
      - "8929:22"      # Git over SSH:clone 地址里会带这个端口
    environment:
      GITLAB_OMNIBUS_CONFIG: |
        external_url 'http://<服务器IP>:8080'
        gitlab_rails['gitlab_shell_ssh_port'] = 8929
    volumes:
      - /opt/gitlab/config:/etc/gitlab
      - /opt/gitlab/logs:/var/log/gitlab
      - /opt/gitlab/data:/var/opt/gitlab
    shm_size: '256m'

这段在干什么:用一个 Docker 容器跑起整个 GitLab。Web 用 8080、Git over SSH 用 8929 而不是标准端口,因为服务器自身的 SSH 已占 22 端口,映射到别的端口避开冲突,代价只是 clone 地址里多个端口号。

三个数据卷 config(配置)、logs(日志)、data(仓库、数据库等全部数据)——备份就是备份这三个目录。首次启动需 3~5 分钟,初始 root 密码在 config/initial_root_password 里,登录后立刻改掉。

GitLab Runner

流水线的执行者 · 干活的蓝领

它是什么

GitLab 本体只负责「调度」(决定该跑什么),真正跑测试、构建、部署的体力活由 Runner 干。它装在 GitLab 同一台服务器上,像个待命工人:GitLab 派单,它开工。

在这套系统里干什么

  • docker 执行器:每个流水线任务跑在一个一次性的 Docker 容器里,跑完即焚,互不污染;挂宿主机的 docker.sock 是为了能在任务里构建镜像;
  • tag 隔离:注册时打上标签 office,deploy,流水线任务可以指定「只让带某标签的 Runner 跑」。这台 Runner 在办公网、拿着部署用的 SSH 钥匙,部署任务必须钉在它身上——这是网络层面的天然隔离;
  • 持有部署钥匙:通过卷挂载把 /opt/runner-ssh/(部署私钥 + ssh config + known_hosts,只读)挂进任务容器;公钥发到各目标机的 deploy 账号和文件服务器的 ops 账号上。
注册 Runner(token 在 GitLab 后台:管理区 → CI/CD → Runners)
sudo gitlab-runner register \
  --url "http://<服务器IP>:8080" \
  --token "<注册token>" \
  --executor "docker" \
  --docker-image "docker:26" \
  --description "office-runner" \
  --tag-list "office,deploy" \
  --docker-volumes "/var/run/docker.sock:/var/run/docker.sock"
/etc/gitlab-runner/config.toml(片段)—— 把部署钥匙挂进任务容器
[[runners]]
  [runners.docker]
    volumes = ["/var/run/docker.sock:/var/run/docker.sock",
               "/opt/runner-ssh:/home/gitlab-runner/.ssh:ro"]

GitLab CI

流水线即代码 · 自动闸门

它是什么

CI(持续集成)的意思:每次代码变更自动跑测试/构建,问题立刻暴露,而不是攒到发布前。GitLab CI 的配置文件是仓库根目录的 .gitlab-ci.yml,和代码一起版本管理——CI 配置的修改也走 MR 评审,这对 AI agent 友好(它会读文件,不需要懂网页操作)。

在这套系统里干什么

  • 三个阶段:test(自动闸门)→ build(产出可部署制品)→ deploy(默认手动触发);
  • 开 MR 时必跑测试 + main 分支每次变更必跑,MR 页面直接显示流水线绿/,红的不能合并;
  • 测试报告(junit)直接显示在 MR 页面,失败定位到具体用例;制品存到 GitLab(保留 30 天),后续阶段和回滚都能取到;
  • ops 仓库也有自己的流水线:ansible-lint--check --diff 预演 → 合并后手动 apply。
.gitlab-ci.yml(语言无关骨架)
stages:
  - test        # 自动化测试:代码质量的自动闸门
  - build       # 构建:产出可部署的制品
  - deploy      # 部署:推到服务器(默认手动触发)

lint:
  stage: test
  script:
    - <你的lint命令>     # 例如 ruff check . / npm run lint
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"   # 开 MR 时跑
    - if: $CI_COMMIT_BRANCH == "main"

unit-test:
  stage: test
  script:
    - <你的单测命令>     # 例如 pytest / npm test / go test ./...
  artifacts:
    reports:
      junit: report.xml   # 测试报告直接显示在 MR 页面
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

build:
  stage: build
  script:
    - <你的构建命令>
    - tar czf app-$CI_COMMIT_SHORT_SHA.tar.gz <产物路径>
  artifacts:
    paths:
      - app-*.tar.gz      # 制品存到 GitLab,回滚也能取到
    expire_in: 30 days
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_COMMIT_TAG  # 打 tag 时构建正式发布版

这段在干什么:三行 stages 定义流水线顺序;每个缩进块是一个「任务」,script 是要执行的命令,rules 控制什么时候跑。注意测试阶段挂在「开 MR」和「main 分支」两个时机——两道自动闸门。

关于测试分层,不必一步到位:单元测试必须有(快、是 MR 的强制闸门);集成测试有则更好(GitLab CI 的 services: 能在任务旁临时起一个 PostgreSQL/Redis 容器,用完即弃);端到端测试可选(慢且脆,建议只在 main 或发版前跑)。skill 里会规定「agent 提交前必须本地先跑一遍单测」,流水线是复核——两道闸门都比人肉检查可靠。

Ansible(重点)

运维自动化 · 黄色管家

它是什么

Ansible 是「批量操作服务器的遥控器」。你写一份 YAML 剧本(playbook),描述「每台机器应该是什么样子」,Ansible 负责通过 SSH 连上去,让现实收敛到你描述的样子。这叫 Infrastructure as Code(基础设施即代码)

它的四个关键特性(也是选它的理由)

  • 无 agent(agentless):目标机什么都不用装,只要 SSH 能通——完美匹配第 04 章的推送模型,内网机器零负担;
  • 声明式 + 幂等:playbook 写的是「服务 X 应处于运行状态」这种目标状态,不是一步步命令;跑一遍和跑十遍结果一样,可以放心反复执行;
  • 纯文本 YAML:AI agent 读写得心应手;
  • 推送式:从内网机器的角度,它什么都不用连。

在这套系统里干什么(四大职责)

  • 部署:CD 流水线的 deploy 阶段就是调 ansible-playbook,把制品推到目标机并重启服务;
  • 常驻服务管理:渲染 systemd 模板 → 推到 /etc/systemd/system/daemon-reload → 确保服务处于期望状态,有变更时重启;
  • 定时任务管理:用 cron 模块统一登记,禁止直接登服务器 crontab -e
  • 脚本下发:脚本母本存 Git,playbook 同步到各机 /opt/ops-scripts/,各机版本永远一致。

核心概念速览

  • inventory:服务器清单,含分组和连接参数。网络拓扑差异全部封装在这里——intranet_prod 组声明了 ansible_ssh_common_args: '-o ProxyJump fileserver',Ansible 连这组机器时自动走跳板,所以同一个 playbook 能部署两种网络的机器
  • group_vars:按组统一配置变量(如上面那行 ProxyJump 参数);
  • cron 模块:Ansible 的 cron 条目带 name 标记,只管理它认识的条目,不动别人手工加的行(但团队纪律是:发现手工条目就登记进仓库后删掉手工版);
  • systemd 模板渲染:服务定义写成 .j2 模板,变量(服务名、用户、启动命令)按机器填值后下发;
  • --check --diff 预演:Ansible 的「排练模式」,列出会改什么但不真改。MR 阶段就能看到「合并这个改动会对服务器造成什么具体影响」,这是运维变更的评审依据;AI agent 部署前也必须先 dry-run 给你看变更清单;
  • 即席命令:不写 playbook 也能临时批量操作,例如:
即席命令示例(内网机器自动走跳板)
ansible office-web1 -m systemd -a "name=app state=restarted"   # 重启某服务
ansible prod -m shell -a "systemctl status app --no-pager"     # 看所有生产机服务状态
ansible intranet-app1 -m command -a "/opt/ops-scripts/cleanup-logs.sh --days 30"
playbooks/cron.yml 的数据(定时任务全部登记在这里)
cron_jobs:
  - name: 清理30天前的日志
    hosts: prod
    user: deploy
    minute: "30"
    hour: "3"
    job: /opt/ops-scripts/cleanup-logs.sh --days 30
  - name: 每日对账
    hosts: intranet-app1          # 只在指定机器跑
    user: deploy
    minute: "0"
    hour: "6"
    job: /opt/ops-scripts/daily-reconcile.sh

这段在干什么:哪台机器、几点、跑什么,Git 里看得清清楚楚,改动有评审、有历史。playbook 循环这份清单调用 cron 模块下发到各机。

SSH 与 ProxyJump

青绿通道 · 一切连接的底座

它是什么

SSH 是远程登录服务器的加密协议,也是整套系统唯一的「搬运通道」——人登录、Ansible 推送、git 推拉代码,全走它。ProxyJump 是它的跳板功能,三步原理和配置已在第 04 章讲透:端到端认证,跳板机只转发流量、看不到内容,也不存任何到内网的私钥

在这套系统里干什么

  • 统一控制流:办公网 → 文件服务器 → 纯内网,一条链路打天下;
  • 主机名前缀约定(office-* / intranet-*)同时被 ssh config 和 Ansible inventory 复用;
  • 安全基线:全部服务器禁用密码登录只许密钥、禁用 root 直登;钥匙按「人/系统」分开(你的、Runner 的、队友的各自独立),泄露一把吊销一把。

systemd

服务器上的服务保姆

它是什么

systemd 是 Linux 自带的服务管理器,负责服务的启动/停止/开机自启/崩溃自动重启。你不需要学它的全部,只要知道:每个常驻服务对应一个 unit 文件,写清楚「用什么用户、在哪个目录、跑什么命令、挂了怎么重启」。

在这套系统里干什么

  • 所有常驻服务(web 应用、worker)都以 systemd 服务形式运行,小团队不上 Kubernetes,用 systemd 足够;
  • unit 文件不是手写的,是 Ansible 用模板渲染后统一下发的;
  • 密钥通过 EnvironmentFile= 注入服务进程,明文只在目标机的 600 权限文件里。
templates/systemd/app.service.j2(.j2 = 待填变量的模板)
[Unit]
Description={{ app_name }}
After=network.target

[Service]
Type=simple
User={{ app_user }}
WorkingDirectory=/opt/{{ app_name }}
ExecStart=/opt/{{ app_name }}/current/<启动命令>
Restart=always
RestartSec=5
EnvironmentFile=/opt/{{ app_name }}/.env    # 密钥从这里注入

[Install]
WantedBy=multi-user.target

这段在干什么:{{ }} 是占位变量,Ansible 按机器填值后推到 /etc/systemd/system/Restart=always 保证崩溃自动拉起;EnvironmentFile 指向部署时生成的 .env,密钥不进 unit 文件本身。

Docker / Docker Compose

容器:把软件打包成标准盒子

它是什么

Docker 把一个软件连同它需要的运行环境打包成一个「标准盒子」(容器),在哪台机器上打开都长一个样;Docker Compose 用一个 YAML 文件描述「起哪几个盒子、怎么连」,一条 docker compose up -d 全部启动。

在这套系统里干什么

  • GitLab CE 本体用 Docker Compose 跑(见上面 GitLab 一节的 compose 文件);
  • Runner 的 docker 执行器:每个流水线任务是一个一次性容器,跑完即焚;
  • 注意:生产部署的制品不是 Docker 镜像——见下面 tar + rsync 一节的解释。

SOPS + age

紫色钥匙 · 密钥加密存 Git

它是什么

SOPS(Mozilla 开源)是文件加密工具,age 是现代加密算法(可理解为 GPG 的极简现代版)。组合起来的思路一句话:把密钥加密后当普通文件存 Git。没有私钥的人拉到仓库也只是一堆乱码。

在这套系统里干什么

  • 业务密钥(数据库密码、API Key、证书)加密后存 ops 仓库的 secrets/,改密钥和改配置一样走 MR,有评审有历史有回滚;
  • 部署时在流水线内用私钥临时解密,只取需要的字段,渲染进目标机的 .env(权限 600),明文永不进 Git、不进日志
  • 编辑体验很好:sops secrets/prod.sops.yaml 自动解密 → 打开编辑器 → 保存时自动重新加密。
secrets/prod.sops.yaml —— 它在 Git 里的样子,全是密文
db_password: ENC[AES256_GCM,data:K8s3j...,type:str]
api_key: ENC[AES256_GCM,data:mV92k...,type:str]
sops:
  age:
    - recipient: age1ql3z7hjy54pw3hyww5sny43kmt9acvz...   # 谁能解密,写在文件里

这段在干什么:每个值都是 ENC[...] 密文;文件尾部的 recipient 记录「用哪把公钥加的密」。这个文件可以放心提交进 Git。

为什么适合 小团队:零常驻服务(Vault 要养一个服务还要管解封,SOPS 就是文件 + 一把钥匙)、密钥跟着代码走、天然防呆(明文根本不存在于 Git 历史里,不可能误提交)。私钥怎么放,见第 07 章。

glab CLI

GitLab 的命令行入口

它是什么

GitLab 官方命令行工具。浏览器里能做的事(看 Issue、开 MR、查流水线),终端里敲命令也能做。

在这套系统里干什么

  • 主要是给 AI agent 用的:agent 用它读 Issue、开 MR、查流水线状态(glab auth login 登录一次即可,也可以用 GITLAB_TOKEN 环境变量 + API);
  • 命令行输出是文本,agent 读起来比解析网页容易得多——这又是「一切对 AI 友好」的一个注脚。

tar + rsync 制品

最朴素的部署包裹

它是什么

「制品」(artifact)是构建产出的可部署文件。这套系统的制品就是最朴素的 tar 压缩包:构建阶段 tar czf app-版本号.tar.gz 打包,部署时 rsync(增量同步工具)推到服务器解压就能跑。

为什么不用 Docker 镜像做制品

纯内网机器访问不到办公网的镜像仓库,用镜像就得再搭内网仓库或走 docker save | ssh | docker load 的中转,多一层复杂度。tar 包两种网络通吃。制品存在 GitLab 里(保留 30 天),回滚 = 重新部署上一个版本的 tar 包。未来办公网服务容器化时,GitLab CE 自带的镜像仓库开箱即用,那时再演进。

Skill 库

AI agent 的操作手册体系

它是什么

Skill 是给 AI agent 的「标准作业程序」:每个 skill 是一个目录,内含 SKILL.md(说明书:何时用、输入什么、一步步怎么做、红线是什么)+ templates/(内嵌模板)+ scripts/(封装脚本)。放在你电脑的 ~/skills/,纳入 Git 版本管理。

为什么不是「给 agent 一堆模板」

裸模板Skill
本质一段静态文本/文件样板一份操作手册:说明书 + 模板 + 脚本 + 检查清单
agent 拿到后不知道何时用、用完做什么、有什么坑知道触发条件、完整步骤、安全边界、验收标准
出错概率高(每次靠 agent 自由发挥串联流程)低(流程被固化,agent 按手册执行)

在这套系统里干什么

  • 凡是需要 agent 做的、有固定套路的操作,都沉淀成 skill;skill 里没有的操作,说明流程还没固化,先固化再交给 agent;
  • skill 与 CI 同源deploy-service 里执行的 ansible 命令和流水线里是同一套 playbook——agent 不是另起炉灶,而是「在你电脑上以你的身份触发同一套代码化流程」;
  • skill 里的「红线」(如「生产部署必须先 dry-run 并经我确认」)是流程约定,让 agent 行为可预期、可审计;
  • 模板统一维护在 skill 里,需要同步到 GitLab 仓库的(如 issue 模板)由对应 skill 负责同步。
skills/deploy-service/SKILL.md(节选)—— 一个 skill 该写到什么颗粒度
---
name: deploy-service
description: 将指定服务部署到指定环境。当用户要求部署、发版、上线、回滚时使用。
---

## 执行步骤
1. 运行 scripts/precheck.sh:确认目标机可达(内网自动走 ProxyJump)、
   磁盘 >20%、负载正常。任一失败则停止并报告。
2. Dry-run:ansible-playbook ... --check --diff,将 diff 摘要汇报给用户。
3. 等待用户明确确认。未确认前禁止进入下一步。
4. 正式执行同一命令(去掉 --check --diff)。
5. 运行 scripts/healthcheck.sh:检查 systemd 状态 + 应用健康接口。
6. 汇报结果。失败时先运行 scripts/rollback.sh 再汇报。

## 红线
- 禁止跳过 dry-run 直接部署生产环境
- 禁止用 shell 模块临时拼凑部署命令,一切走 deploy-app.yml
- 部署涉及密钥时只通过 sops 解密注入,禁止把明文写入任何日志或回复

这段在干什么:注意写法——步骤可机械执行、每步有验证、失败有兜底、红线明确。agent 的执行质量取决于手册写得多清楚。

12 个 skill 的完整规划表见第 08 章。

06 / END TO END

串起来:一个 bug 的一生

这一章把前面所有零件串成一条完整流水线:从有人提 bug,到修复上线,每一步谁在干活、经过哪些颜色。点击步骤条逐步看。

图 2 · 数据流(当前步骤高亮)
1 提 Issue 2 分支代码 3 CI 测试 4 评审合并 5 构建制品 6 手动部署 7 健康上线

第 1 步 · 提 Issue

有人在 GitLab 上建 Issue(或 AI agent 通过 API 帮你建),按模板写清复现步骤/期望行为/实际行为,打上 bug 标签和优先级。Issue 是一切的起点——没有 Issue 就没有后续的分支、MR 和部署。

涉及角色:GitLab(Issues)

AI agent 协作闭环长这样

上面 7 步里,人的动作只有三个:提需求、评审点合并、确认部署。其余的重复劳动,你电脑上的 AI agent 都能按 skill 手册干完。源文档给的典型闭环对话是这样的:

你:「处理一下 Issue #128,登录超时那个 bug。」

agent(加载 work-on-issue skill):读 Issue → 建 fix/128-login-timeout 分支 → 改代码 → 本地跑单测通过 → 推送 → 开 MR(描述按模板写好,自动关联 Issue)→ CI 自动跑测试 → 汇报你「MR 已就绪,测试全绿,请评审」。

评审合并、打 tag → CI 构建出制品 → 出现部署按钮。

你:「部署到内网生产。」

agent(加载 deploy-service skill):precheck → dry-run 给你看变更 → 你确认 → 经文件服务器跳板推到内网机 → 健康检查 → 「部署完成,v1.4.1 已上线,服务健康。」

注意两个设计细节:一是 推送模型的附带好处——CI 能做的事,agent 在你电脑上全能做,反之亦然,因为用的是同一套 playbook,不会出现「CI 里一套、本地一套」;二是 agent 的全部操作都落在 git 提交和 MR 上,天然可审计

07 / SECRETS

密钥怎么管

这一章回答「密码、私钥放哪才安全」:先把密钥分三类(不同类型的管理方式完全不同),再看私钥这把「钥匙的钥匙」怎么分布。

密钥三类分治

类别例子放哪
A. 业务密钥(要落到服务器上的) 数据库密码、第三方 API Key、证书 SOPS 加密后存 ops 仓库的 secrets/,部署时解密注入
B. 流水线密钥(只在 CI 里用的) age 私钥本身、临时用的 token GitLab CI/CD Variables(页面设置,不进 Git),勾 Masked + Protected
C. 身份密钥(SSH 私钥等) 你的个人私钥、Runner 的部署私钥 各自所在机器的本机,永不进 Git、永不上网盘

age 私钥的分布(整个密钥体系最要紧的一件事)

密钥「落盘即结束」:部署时 sops -d --extract 只解密需要的字段,渲染进目标机的 /opt/app/.env(权限 600,属主 deploy),服务通过 systemd 的 EnvironmentFile= 读取。明文只存在于目标机的 600 权限文件和内存里——不进 Git、不进日志(Ansible 任务记得设 no_log: true)。

轮换也很简单:改密钥 = sops 编辑 → MR(diff 显示的是密文变化,安全)→ 合并 → 重跑部署 playbook → 重启服务,全程可审计。人员变动、怀疑泄露时轮换,每半年例行一次。

08 / SINGLE SOURCE OF TRUTH

一切皆有出处:两个仓库的目录结构

这一章回答「东西都放在哪」:代码在项目仓库,服务器的一切在 ops 仓库,agent 的手册在 skills 目录。看完你就拿到了整套系统的「地图」。

ops 仓库:运维的唯一事实来源

从此以后,任何对服务器的改动 = 改这个仓库 + 跑 Ansible,禁止登服务器手工改。

ops/ 目录树
ops/
├── ansible.cfg                  # Ansible 全局配置
├── inventories/
│   └── prod/
│       ├── hosts.ini            # 所有服务器的清单(含网络分组)
│       └── group_vars/          # 按组的变量(如内网组统一配 ProxyJump)
│           ├── office_prod.yml
│           └── intranet_prod.yml
├── playbooks/
│   ├── deploy-app.yml           # 部署应用
│   ├── services.yml             # 管理常驻服务(systemd)
│   ├── cron.yml                 # 管理定时任务
│   ├── scripts.yml              # 下发/更新运维脚本
│   └── bootstrap.yml            # 新服务器初始化
├── templates/
│   └── systemd/                 # systemd 服务文件模板
│       └── app.service.j2
├── files/
│   └── scripts/                 # 要下发到服务器的脚本的"母本"
│       ├── cleanup-logs.sh
│       └── sync-data.sh
└── secrets/                     # SOPS 加密的密钥文件
    └── prod.sops.yaml
inventories/prod/hosts.ini
# 办公网生产机
[office_prod]
office-web1
office-web2

# 纯内网生产机
[intranet_prod]
intranet-app1
intranet-db1

# 所有生产机
[prod:children]
office_prod
intranet_prod

这段在干什么:把服务器按网络分两组,名字直接复用 ssh config 里的 office-* / intranet-* 前缀。配合 group_vars/intranet_prod.yml 里的一行 ansible_ssh_common_args: '-o ProxyJump fileserver',内网组自动走跳板——网络拓扑的差异被完全封装在 inventory 里,部署逻辑一份代码、两种环境复用。

ansible.cfg 里有两条值得知道的配置:host_key_checking = True(生产环境不要关主机指纹校验)和 pipelining = True(减少 SSH 往返,跑得快)。

skills 目录:agent 的手册架

~/skills/ 结构(每个 skill 一个目录)
skills/
└── deploy-service/                # skill 名 = 目录名
    ├── SKILL.md                   # 说明书:何时用、输入什么、一步步怎么做、红线是什么
    ├── templates/                 # 该操作需要的模板(systemd unit、MR 描述等)
    └── scripts/                   # 封装好的可执行脚本(precheck.sh、rollback.sh)

12 个 skill 的规划

落地时不必一次写全,第 5 周先写最高频的 5 个(work-on-issue、deploy-service、service-ops、cron-ops、secret-ops),其余随用随补:

Skill干什么内含模板/脚本
issue-triage读 Issue → 补全信息(按模板追问缺失项)→ 打标签/排优先级 → 拆任务issue 模板、标签规范
work-on-issue领 Issue → 建规范分支 → 实现 → 本地跑测试 → 提交 → 推送开 MRcommit 规范、MR 描述模板
review-mr拉取 MR → 对照 checklist 评审(正确性/安全/测试覆盖)→ 留意见评审 checklist
fix-ci流水线红了 → 拉日志 → 定位 → 修复 → 重跑验证常见失败模式清单
release汇总变更 → 定版本号 → 打 tag → 建 Release → 触发部署流水线Release notes 模板
deploy-service部署某服务到某环境:先 dry-run 出变更清单 → 汇报 → 你确认 → 执行 → 验证健康precheck/healthcheck 脚本、回滚脚本
service-ops查状态/重启/看日志(封装 ansible -m systemd 等)常用命令封装
cron-ops增删改定时任务:一律改 ops 仓库走 MR,禁止直接改目标机cron 条目模板
script-ops新增/更新脚本 → 下发 → 在指定机器执行并回收输出脚本样板(带日志/错误处理)
secret-opssops 加解密/轮换/新增字段;检查「明文是否误进 Git」sops 操作流程
server-onboard新机器接入:建 deploy 账号、发公钥、装基础组件、登记进 inventorybootstrap playbook 片段
incident-response线上故障:收集状态/日志 → 初步定位 → 给出处置建议 → 事后整理记录故障报告模板
09 / ROAD NOT TAKEN

为什么不选那些「明星工具」

这一章回答「别人都用的 X,我们为什么不用」:每个被劝退的工具都很有名,但对 小团队来说,不选它们的理由都很具体。

明星工具它很好,但是……
Gitea / Forgejo + Woodpecker CI 比 GitLab 轻量(内存占用约为 GitLab 的 1/10),但 Issue 功能弱、CI 是另一套系统要学。小团队,「少维护一套系统、少学一套概念」比「省内存」更重要。GitLab CE 一台 4C8G 的机器就跑得很舒服。
Jenkins 功能强但插件体系老旧,配置靠点页面、难以代码化,对 AI agent 不友好——agent 擅长读写文本文件,不擅长点网页。
HashiCorp Vault 密钥管理的「重武器」,需要常驻服务、要处理初始化/解封/高可用,小团队维护成本过高。SOPS 是「把密钥加密后当普通文件存 Git」,零常驻服务。
Rundeck / Cronicle 定时任务面板 功能与 Ansible 重叠。定时任务用面板管、部署用 Ansible 管,就有了两个「事实来源」。我们选择一切代码化存 Git,Git 仓库是唯一事实来源。
Kubernetes 小团队 + 少量服务器,上 K8s 是给自己找班加。用 systemd 管理服务足够。

共同的原则只有一条:每多一个系统,就多一份要维护的东西、多一个「事实可能不一致」的地方。这套设计宁愿用朴素的工具把流程打通,也不用强大的工具把复杂度搬回家。

10 / ROADMAP

五周落地路线图

这一章回答「从哪开始动手」:不必一次到位,按依赖关系分五周走,每周结束都有可用的成果。

目标具体动作验收标准
第 1 周 平台就位 装 GitLab CE;建群组/项目/成员;迁移代码仓库;配保护分支和 MR 规则 团队都能推拉代码、开 MR
第 2 周 CI 跑起来 装 Runner;给主项目写 .gitlab-ci.yml 的 test/build 阶段;补 issue 模板和标签 MR 上能看到测试自动跑,且红的合不进
第 3 周 运维代码化 建 ops 仓库;盘点所有服务器登记进 inventory;服务/定时任务/脚本逐台迁进 Ansible;配好 ssh config 和 ProxyJump 不再登服务器手工改任何东西;ansible prod -m ping 全通(含内网)
第 4 周 CD 与密钥 写 deploy playbook 并接入流水线;初始化 SOPS+age,把散落密钥收编进 secrets/;配 CI 变量 打 tag 后能一键部署到两种网络的机器;Git 里搜不到明文密钥
第 5 周 Agent 上岗 按规划建 skill 库(先写最高频的 5 个);配好本机环境(glab、Ansible、sops、SSH) agent 独立完成「领 Issue → MR → 部署」全流程一次

之后的迭代方向(用到再加,不提前建设):监控告警(Uptime Kuma,一个容器就够)、预发(staging)环境、更多测试分层、更多 skill。

11 / GLOSSARY

术语速查

看到黑话回来查这张表。每条都用人话解释。

术语大白话解释
CI(持续集成)每次代码变更自动跑测试/构建,问题立刻暴露,而不是攒到发布前
CD(持续部署/交付)通过流水线把构建产物部署到服务器;本系统采用「自动到门前、人工按按钮」
MR(Merge Request)合并请求:把分支合进 main 之前的评审单,带讨论、测试状态、diff
Pipeline(流水线).gitlab-ci.yml 定义的一串自动化阶段(test → build → deploy)
Runner实际执行流水线任务的工人进程,装在办公网 GitLab 服务器上
Artifact(制品)构建产出的可部署文件(本系统是 tar 包),GitLab 负责存
Git代码版本管理工具:每次修改存成快照,谁改了什么全部可查可回退
Docker / 容器把软件连同运行环境打包成「标准盒子」,在哪台机器打开都一个样
SSH远程登录服务器的加密协议;本系统里人、Ansible、git 全走它
ProxyJumpSSH 的跳板功能:经中间机转发,端到端认证,跳板机看不到内容
Ansible / Playbook无代理的运维自动化工具 / 用 YAML 写的「操作剧本」,描述目标状态
InventoryAnsible 的服务器清单,含分组和连接参数
幂等同一操作执行 N 遍和执行 1 遍效果相同,可以放心重跑
systemdLinux 的服务管理器,负责服务的启动/停止/开机自启/崩溃重启
SOPS / age文件级加密工具 / 现代加密算法;组合实现「密钥加密存 Git」
Skill给 AI agent 的操作手册目录:SKILL.md(说明)+ 模板 + 脚本
IaC(基础设施即代码)把服务器该是什么样子写成代码存 Git,工具负责落实
唯一事实来源(SSOT)任何信息只有一个权威出处(本系统 = Git 仓库),其余都是它的投影