祖传运维脚本别一把火烧了!迁移 GitOps 的 7 步清单

简介: 祖传运维脚本别一把火烧了!迁移 GitOps 的 7 步清单

祖传运维脚本别一把火烧了!迁移 GitOps 的 7 步清单

作者:Echo_Wish
说明:文中的脚本、仓库、公司场景均为假设的典型案例,不对应任何真实企业。三个 Python 工具我在沙箱里用样例数据实际跑过,输出原样贴在文中;但沙箱里没有 kubectl / kustomize / Argo CD,所以文中的 YAML 只做了语法解析,没有在真实集群里验证,上线前请自己 kustomize build 和 argocd app diff 过一遍。

凌晨两点,订单接口扛不住了。值班同学 ssh 上跳板机,敲了一行 kubectl scale --replicas=8,流量下去了,大家睡觉。

三个礼拜后,有人按 Git 里的清单重新发了一次版。副本数悄悄变回 6。凌晨那场事故,原样重演了一遍。

这事儿怪谁?谁都不怪。病根在于:线上长什么样,只有"上一个动手的人"知道。

这就是我们要聊的 GitOps。说白了就一句话:线上长什么样,以 Git 为准;想改线上,先改 Git。

注意,它不是"装个 Argo CD"。工具谁都会装。真正难的是改一条规矩:谁有资格改生产?

下面是我总结的 7 步走查清单。每一步我都会告诉你:做什么、为什么、怎么算做完。

第 1 步:先盘点,别急着动手

迁移之前,先给手上的祖传脚本做个"体检"。核心就问一个问题:这个脚本,是在描述一个状态,还是在执行一个动作?

我写了个小工具,按"它到底在对线上做什么"给脚本打标签:

RULES = [
    ("改工作负载", r"\bkubectl\b.*\b(set image|scale|rollout|apply|patch|edit)\b", "可迁:变成 Deployment 里的声明"),
    ("改配置",     r"\bkubectl\b.*\b(patch|edit)\b.*\b(cm|configmap)\b",           "可迁:变成 ConfigMap 清单"),
    ("Helm 安装",  r"\bhelm\b.*\b(install|upgrade)\b",                            "可迁:变成 HelmRelease / Application"),
    ("清理删除",   r"\bkubectl\b.*\bdelete\b|\bfind\b.*-delete",                  "别直接迁:改成 CronJob 或 TTL 机制"),
    ("数据库变更", r"\b(psql|mysql)\b.*(-f|-c)\b|\bUPDATE\b|\bALTER\b",           "别迁:一次性、不可逆,GitOps 管不了数据"),
    ("远程跳板",   r"\bssh\b|\bscp\b",                                            "必须消灭:谁能 ssh 谁就能绕过仓库"),
    ("明文密钥",   r"(password|passwd|secret|token)\s*[=:]\s*\S+",                "必须先处理:先轮换,再进密钥管理"),
]
def verdict(tags: Counter) -> str:
    if tags["数据库变更"]:
        return "🚫 不迁(保留为受控任务)"
    if tags["明文密钥"] or tags["远程跳板"]:
        return "⚠️ 先整改再迁"
    if tags["清理删除"]:
        return "🔁 改造成 CronJob"
    return "✅ 可直接声明化"

对我造的 4 个假设脚本跑一遍:

脚本                    结论                          命中标签
------------------------------------------------------------------------------
db_migrate.sh         🚫 不迁(保留为受控任务)               数据库变更×2
deploy_order.sh       ⚠️ 先整改再迁                    改工作负载×3、远程跳板×2、改配置×1
hotfix_nginx.sh       ⚠️ 先整改再迁                    改工作负载×2、改配置×1、Helm 安装×1、明文密钥×1
nightly_cleanup.sh    🔁 改造成 CronJob               清理删除×3

我故意把样例造得"很祖传",所以 4 个里没有一个能直接搬。真实情况肯定因团队而异,但我的经验是:盘点这一步,十有八九会翻出一两个"明文密码"和一两个"没人敢动"的脚本。

这个工具是正则匹配,会误判也会漏判,别当真理,只当"体检的第一遍"。它最大的价值是逼你把每个脚本过一遍,给出结论:迁、改造,还是别碰。

特别提醒两个:

  • 带密码的脚本,先轮换密码,再谈迁移。 密码既然写进过脚本、进过 bash history,就该当它已经泄露过。
  • 数据库变更别硬塞进 GitOps。 它是一次性、不可逆、顺序敏感的,和"声明一个期望状态"不是一回事。

第 2 步:把"动作"改写成"状态"

这是整个迁移里最关键的一步,也是思维转变最大的一步。

先看祖传发布脚本长什么样:

#!/bin/bash
# 假设的"祖传"发布脚本:谁都不敢删,谁都不知道还有没有人在用
set -e
VERSION=$1
ssh ops@prod-jump "kubectl -n shop set image deploy/order-api order-api=registry.example.com/shop/order-api:${VERSION}"
ssh ops@prod-jump "kubectl -n shop scale deploy/order-api --replicas=6"
kubectl -n shop patch cm order-config --type merge -p '{"data":{"FEATURE_X":"on"}}'
echo "deployed ${VERSION}"

脚本在回答"怎么做":先改镜像,再扩副本,再改配置。再跑一遍会怎样?不知道。中途断了呢?不知道。

清单在回答"要什么":

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
images:
  - name: registry.example.com/shop/order-api
    newTag: v1.8.2          # 发版 = 改这一行 + 提 PR,取代 deploy_order.sh 里的 set image
replicas:
  - name: order-api
    count: 6                # 取代 deploy_order.sh 里的 scale --replicas=6

这就是所谓的幂等:用人话说,跑一次和跑一万次,结果一样。 发版从"执行一个脚本",变成了"改一行字,提一个 PR"。

再看那个定时清理脚本。很多人的第一反应是:把 kubectl delete job 翻译成一个定时删除任务。我的看法是,别翻译动作,要翻译意图。 它的意图是"报表 Job 跑完不要永远留着",那就直接声明:

# 取代 nightly_cleanup.sh 里的 "kubectl delete job -l app=report":
# 不再靠脚本去"删",而是声明"这个 Job 完成一天后自己消失"
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
  namespace: shop
spec:
  schedule: "0 2 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      ttlSecondsAfterFinished: 86400
      template:
        metadata:
          labels: {
    app: report }
        spec:
          restartPolicy: Never
          containers:
            - name: report
              image: registry.example.com/shop/report:v2.3.0
              resources:
                limits: {
    cpu: 500m, memory: 512Mi }

验收标准:同一份清单反复 apply,线上结果不变。

第 3 步:定好仓库的"户口本"

清单有了,放哪儿?我推荐的结构很朴素:

gitops-repo/
├── apps/
│   ├── order-api/
│   │   ├── base/              # 所有环境共用
│   │   └── overlays/prod/     # 只写差异:版本、副本数
│   └── report/
├── clusters/prod/             # 告诉控制器"这个集群装哪些应用"
├── platform/                  # RBAC、命名空间等平台级资源
└── tools/                     # policy_check.py、drift_check.py

三个原则:

  • 配置仓库和应用代码仓库分开。 代码仓库每次提交都触发构建,配置仓库只记录"线上想要什么"。分开之后,两边的审批人和节奏都可以不同。
  • 环境之间只写差异。 base 放共用的,overlays/prod 只写"版本是多少、副本几个"。新增一个环境,就是新增一个 overlay 目录。
  • 一个目录,对应一个能独立回滚的东西。 别把整个公司塞进一个 YAML。

第 4 步:接入控制器,但先别让它动手

很多团队翻车就翻在这一步:Argo CD 一装,直接开 automated + prune,然后第一次同步就把线上"手改过的"东西全抹了。

正确姿势是先只读:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: order-api-prod
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/platform/gitops-repo.git   # 假设的仓库地址
    targetRevision: main
    path: apps/order-api/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: shop
  syncPolicy:
    # 第一阶段:什么都别开,只看 OutOfSync,人工点 Sync
    # 第二阶段:稳定一两周后再打开下面这段
    # automated:
    #   prune: true       # Git 里删了,线上也删(上线前先想清楚!)
    #   selfHeal: true    # 有人手改线上,自动改回 Git 的样子
    syncOptions:
      - CreateNamespace=true

这里有个反直觉的观点:第一次接入,最有价值的产出不是自动部署,而是那份 diff。

控制器告诉你"Git 里写的"和"线上跑的"哪些不一样。每一条差异,背后都是一次以前没人记录的手改。把这些差异逐条搞清楚:该进 Git 的补进去,不该存在的清掉。能解释清楚每一处 OutOfSync,才算准备好开自动同步。

第 5 步:给合并请求装一个"门卫"

仓库成了唯一真相,那"往仓库里合东西"就成了新的生产入口,必须设卡。

CI 里先渲染,再过门卫:

name: gitops-gate
on:
  pull_request:
    paths: ["apps/**", "clusters/**"]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 渲染所有环境(渲染失败 = 清单写坏了)
        run: |
          mkdir -p /tmp/rendered
          for env in prod; do
            kubectl kustomize apps/order-api/overlays/$env > /tmp/rendered/order-api-$env.yaml
          done

      - name: 策略门卫
        run: |
          pip install pyyaml
          python tools/policy_check.py /tmp/rendered

门卫本身我只写了四条规则,每条都对应一个真实会出事的低级错误:

def containers(doc: dict):
    spec = (doc.get("spec") or {
   }).get("template", {
   }).get("spec", {
   })
    yield from spec.get("containers", []) + spec.get("initContainers", [])


def check_doc(doc: dict) -> list[str]:
    kind, name = doc.get("kind", "?"), (doc.get("metadata") or {
   }).get("name", "?")
    who, errs = f"{kind}/{name}", []

    if kind in ("Deployment", "StatefulSet"):
        if "replicas" not in (doc.get("spec") or {
   }):
            errs.append(f"{who}: 没有显式 replicas")
        for c in containers(doc):
            image = c.get("image", "")
            last = image.rsplit("/", 1)[-1]
            if ":" not in last and "@sha256:" not in image:
                errs.append(f"{who}/{c.get('name')}: 镜像没有 tag({image})")
            elif last.endswith(":latest"):
                errs.append(f"{who}/{c.get('name')}: 镜像用了 latest({image})")
            if not (c.get("resources") or {
   }).get("limits"):
                errs.append(f"{who}/{c.get('name')}: 缺 resources.limits")

    if kind == "Secret" and (doc.get("data") or doc.get("stringData")):
        errs.append(f"{who}: Secret 带了明文/base64 数据,请改用 SOPS / SealedSecret")
    return errs

拿一份故意写坏的清单试试(镜像用 latest、没写 replicas、没有资源限制、还塞了个 Secret):

❌ manifests/bad/order-api.yaml: Deployment/order-api: 没有显式 replicas
❌ manifests/bad/order-api.yaml: Deployment/order-api/order-api: 镜像用了 latest(registry.example.com/shop/order-api:latest)
❌ manifests/bad/order-api.yaml: Deployment/order-api/order-api: 缺 resources.limits
❌ manifests/bad/order-api.yaml: Secret/db-pass: Secret 带了明文/base64 数据,请改用 SOPS / SealedSecret

检查 2 个资源,发现 4 个问题

脚本以非 0 退出码结束,CI 就是红灯,合不进去。

我想强调的是:规则宁少勿多。 四条没人绕得过的规则,胜过四十条没人读的规则。latest 镜像尤其致命:它让"同一份 Git 提交,今天和明天部署出来的东西不一样",等于直接废掉了 Git 作为真相源的意义。

生产里建议用 Kyverno、OPA/Conftest 这类成熟方案,我这个脚本只讲思路,别拿去当最终方案。

第 6 步:密钥进仓库要加密,权限要收回来

这一步是整个清单里最容易被跳过、也最不能跳的。

先说密钥。 清单要进仓库,密码就不能裸着进。用 SOPS 加密,只加密值,保留结构,PR 里的 diff 还能看懂:

# 密钥进仓库的前提:加密。这里用 SOPS + age(公钥是占位符,换成你自己的)
creation_rules:
  - path_regex: .*\.secret\.yaml$
    encrypted_regex: ^(data|stringData)$      # 只加密值,保留结构,PR 里 diff 还看得懂
    age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

再说权限。 前面五步,都是在"让正确的路好走"。这一步,是"让错误的路走不通":

# 第 6 步的关键一刀:人只能看,不能改。改,只能走 PR。
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sre-readonly
  namespace: shop
subjects:
  - kind: Group
    name: sre
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view              # 内置只读角色,且不含 Secret 的读取权限
  apiGroup: rbac.authorization.k8s.io

我的看法很直接:权限不收回,GitOps 就只是一份"建议"。 只要还有人能 kubectl edit 生产,仓库里写的就是"参考答案",不是"事实"。

这一步最难的不是技术,是人。收权限那天,一定有人说"那出紧急故障怎么办"。答案是:给紧急通道,但让它留痕。 比如一个需要审批、限时生效的 break-glass 账号,用完必须补一个 PR 把改动写回仓库。紧急可以绕过流程,但不能绕过记录。

第 7 步:定期查漂移,定期演练回滚

最后一步,是让整套机制"保鲜"。

漂移,就是 Git 里写的和线上跑的不一致。Argo CD 的 selfHeal 能自动处理,但你至少要看懂它在比什么。我写了个小脚本,只比"人最容易手滑去改"的几个字段:

# 只比"人会手滑去改"的字段;比整份对象会被控制器写入的 status、时间戳、uid 淹没
def pick(deploy: dict) -> dict:
    spec = deploy.get("spec", {
   })
    pod = spec.get("template", {
   }).get("spec", {
   })
    out = {
   "replicas": spec.get("replicas")}
    for c in pod.get("containers", []):
        n = c["name"]
        out[f"container[{n}].image"] = c.get("image")
        out[f"container[{n}].limits"] = (c.get("resources") or {
   }).get("limits")
    return out


def diff(desired: dict, live: dict) -> list[str]:
    d, l = pick(desired), pick(live)
    lines = []
    for k in sorted(set(d) | set(l)):
        if d.get(k) != l.get(k):
            lines.append(f"  {k}\n      Git : {d.get(k)}\n      线上: {l.get(k)}")
    return lines

我用一份"有人半夜手改过"的线上快照(副本数改成 8、镜像换成 hotfix 版)对比 Git:

⚠️  order-api: 发现 2 处漂移(有人绕过了 Git)
  container[order-api].image
      Git : registry.example.com/shop/order-api:v1.8.2
      线上: registry.example.com/shop/order-api:v1.8.3-hotfix
  replicas
      Git : 6
      线上: 8

线上和 Git 一致时,它输出 ✅ order-api: 无漂移,退出码为 0;有漂移退出码为 2,方便你接到定时任务和告警里。

回滚才是 GitOps 最性感的地方。出问题了,不用翻聊天记录找"上个版本是多少":

git revert <出问题的提交SHA>
git push origin main
# 控制器同步之后,线上回到上一个版本

回滚,就是一次 git revert。谁改的、为什么改、怎么改回来,全在提交记录里。

但我要说句实话:没演练过的回滚,等于没有回滚。 建议定期挑个业务低峰,故意往测试环境合一个坏提交,然后按文档回滚一遍。你会发现文档里至少有一步是过期的。

几个反直觉的提醒

第一,别急着删旧脚本。 迁完不要立刻删,先让它变成"只读 + 报警":谁还在调用它,就通知谁。观察一段时间没人用了,再删。脚本的消亡应该有一个观察期,不是一场葬礼。

第二,GitOps 会让你的第一次"事故"变多。 因为以前的手改全被翻出来了。这不是它带来的问题,是它把本来就存在的问题照亮了。

第三,不是所有东西都该进 GitOps。 数据库变更、一次性的数据订正、强依赖顺序的运维动作,硬塞进去只会让清单变得别扭。这类事情更适合走受控的、有审批和审计的任务流程。

第四,小团队别硬上。 只有一个集群、两三个服务、三五个人,一套完整的 GitOps 可能是在用大炮打蚊子。先把第 1、2、5 步做了,已经能拿到大部分收益。

7 步走查清单(可直接抄)

步骤 一句话 做完的标志
1 盘点 给每个脚本打标签 每个脚本都有结论:迁 / 改造 / 不迁
2 声明化 动作改成状态 同一份清单反复 apply,结果不变
3 仓库 配置与代码分离,环境只写差异 新增环境 = 新增一个 overlay
4 控制器 先只读,先看 diff 能解释每一处 OutOfSync 的原因
5 门卫 PR 必过检查 坏清单合不进 main
6 收权 密钥加密,人只读 没人需要、也没人能 kubectl edit 生产
7 保鲜 查漂移,演练回滚 回滚就是一次 git revert

最后

回头看这 7 步,真正难的不是 Kustomize 怎么写,也不是 Argo CD 怎么配。难的是:把"我知道线上是什么样",变成"仓库知道线上是什么样"。

迁移成功的标志,也不是旧脚本删光了,而是半夜出事的时候,你的第一反应是打开仓库,而不是打开终端。

线上的真相只能有一个来源。别让它住在某个人的 bash history 里。

目录
相关文章
|
12天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
4789 29
|
12天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
4521 51
|
7天前
|
存储 弹性计算 人工智能
2026 年阿里云双十一活动入口攻略,新客服务器优惠通道、三大会场入口梳理
综合来看,2026年阿里云双十一活动入口清晰: 阿里云轻量应用服务器、阿里云ECS云服务器、阿里云GPU云服务器、阿里云百炼大模型服务平台 活动会场、综合权益中心、99计划专场三大入口覆盖服务器、AI权益与续费特价;新用户通过控制台首页banner、权益中心、产品页三处路径进入专属专场,老用户通过控制台弹窗和费用中心权益中心进入老客专区,企业用户做企业实名后解锁专属满减与阶梯折扣。
830 0
|
13天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
2176 7
|
7天前
|
云安全 人工智能 安全
|
7天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
1531 3
|
4天前
|
人工智能 安全 API
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(dsh)是DeepSeek AI于2026年开源的轻量级Agent运行框架,基于Cordis内核,践行“一切皆插件”理念。支持Web/桌面双端,开箱即用;内置多模型接入、丰富插件生态(超3.5万),适配编程、办公、创作等场景。(239字)
1046 0
|
13天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
2204 6
|
6天前
|
人工智能 并行计算 调度
本地 AI 一键部署工具 LocModel:Windows 端集成画图、视频生成、Agent 的开源模型运行方案
LocModel 是专为 Windows 设计的一站式本地 AI 工具,支持零配置启动、自动硬件检测、GGUF 模型一键加载、文生图/视频生成及 Agent 自动化任务。适配 8G 显存,内置显存分片调度,轻松实现 AI 漫剧批量创作与离线大模型调用。(239 字)

热门文章

最新文章