1. 什么是 GitOps不是新名词而是运维思维的“归位”你肯定听过 DevOps——它把开发和运维拧成一股绳用自动化流水线把代码从 IDE 直接送到生产服务器上你也可能用过 MLOps它把机器学习模型的训练、验证、上线、监控全链路串起来让算法工程师不再只管调参也得对线上效果负责。但当你在 Kubernetes 集群里手动执行kubectl apply -f config.yaml或者半夜被告警叫醒去修复一个被误删的 ConfigMap 时有没有想过为什么我们能用 Git 管理几千行 Python 代码却要靠人肉kubectl edit去改生产环境的配置这就是 GitOps 要回答的根本问题。GitOps 不是又一个带“Ops”的 buzzword它是一次运维权力的重新分配——把基础设施的“定义权”和“决策权”从运维人员的手动操作、临时脚本、甚至共享文档里彻底收回到 Git 仓库这个唯一可信源Single Source of Truth中。它不发明新工具而是把 Git 这个程序员每天用十几次的协作系统变成整个云原生基础设施的“操作系统内核”。你提交一次 PR不只是改了应用逻辑更是向集群发出了一条不可篡改的“宪法修正案”从此刻起这个命名空间里的所有 Deployment 必须是 v1.2.3 版本Ingress 必须启用 TLS 1.3ServiceAccount 必须绑定特定 RBAC 规则。集群本身会持续“阅读”这份宪法并自动执行、校验、修复——这才是 GitOps 的灵魂声明式 持续调谐Continuous Reconciliation。我第一次在真实项目里落地 GitOps 是给一家做智能客服 SaaS 的客户做灾备重构。他们之前用 Ansible 脚本批量部署几十个 Kubernetes 集群每次版本升级都要人工核对 200 行 YAML 变更出过三次因漏改一个 namespace 导致灰度流量全部打到旧集群的事故。改成 GitOps 后我们把所有集群的 Helm Release 清单、Kustomize base/overlays、甚至 Prometheus 告警规则都放进一个私有 Git 仓库。运维同学只需要在 GitHub 上点开 PR看一眼 diff 就知道这次变更影响哪几个环境、修改了哪些资源类型、是否包含敏感字段比如 Secret 的 base64 值然后点击 Merge。5 分钟后Argo CD 自动拉取变更、校验签名、执行部署、等待健康检查通过——整个过程像合并一段业务代码一样自然。最让我意外的是开发同学开始主动给 infra 目录提 PR因为他们发现改一个 ingress host 名字比找运维开权限、等排期、再手动执行命令快得多。GitOps 的本质是让“基础设施即代码”这句话真正落地为一种可协作、可审计、可回滚的日常实践而不是挂在墙上的流程图。2. GitOps 的核心设计与思路拆解为什么必须是 Git为什么必须是声明式2.1 为什么 Git 是不可替代的“唯一真相源”很多人第一反应是“用数据库存配置不行吗用 Consul 或 Etcd 不更实时” 这是个好问题但恰恰暴露了对 GitOps 本质的误解。GitOps 的核心诉求从来不是“快”而是“可追溯、可协作、可验证、可回滚”。我们来逐条拆解可追溯AuditabilityGit 的 commit hash 是天然的时间戳内容指纹。当线上出现故障你不需要翻 Slack 记录或问“谁昨天改了 config”直接git blame config/nginx-ingress.yaml就能看到每一行是谁、什么时候、为什么这么改附带完整的 commit message 和关联的 Jira ticket。而数据库记录只有“最后更新时间”Consul 的 key-value 变更日志是二进制流无法语义化解读。可协作CollaborationPull Request 是现代软件工程最成熟的协作范式。一个网络策略变更安全团队可以 Review 是否开放了高危端口SRE 团队可以确认是否符合基线标准开发团队能理解这个变更如何影响自己的服务。这种基于 diff 的异步评审远比“所有人挤在 Zoom 里听运维念 YAML”高效且可靠。我见过最典型的反例某金融客户用 Jenkins Pipeline 直接调用 Terraform apply所有配置变更都藏在 pipeline 脚本里。当需要回滚时他们发现脚本里硬编码了环境变量而那个变量值在三个月前的某次 CI 失败日志里才出现过。可验证VerifiabilityGit 支持 GPG 签名、分支保护策略如 require 2 reviewers、commit rule如 Conventional Commits。这意味着你可以强制要求所有生产环境变更必须由 SRE 主管签名所有涉及 Secret 的 PR 必须经过安全扫描所有 Helm Chart 升级必须包含BREAKING CHANGE:标注。这些规则在数据库或配置中心里实现成本极高而在 Git 中是开箱即用的能力。可回滚Reversibilitygit revert commit是原子操作它生成一个新 commit 来抵消旧变更保留完整历史。而数据库 rollback 可能破坏外键约束Etcd 的 delete 操作不可逆。更重要的是GitOps 工具如 Argo CD的回滚就是git reset --hard old-commitgit push整个集群状态会在几分钟内恢复到指定版本——这比从备份恢复 etcd 集群快一个数量级。提示GitOps 不等于“把 YAML 文件扔进 Git 就完事”。真正的 GitOps 要求 Git 仓库结构本身体现环境治理逻辑。例如我们团队强制采用environments/env/clusters/cluster/applications/目录结构每个环境目录下有kustomization.yaml定义该环境所有应用的基线版本sync-policy.yaml定义同步频率和超时策略。这样git log environments/prod/就是生产环境的完整变更史无需任何额外工具。2.2 为什么声明式Declarative是 GitOps 的技术基石这是最容易被忽略的关键点。很多团队尝试 GitOps 时第一步就走偏他们把 Ansible Playbook、Shell 脚本、甚至kubectl create命令的输出结果塞进 Git 仓库。这看似“用了 Git”实则是披着 Git 外衣的“脚本运维”完全违背 GitOps 原则。声明式的核心在于你只描述“系统应该是什么状态”而不关心“如何达到那个状态”。举个具体例子# 声明式告诉集群“我要一个 3 副本的 Nginx带健康检查” apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.21 livenessProbe: httpGet: path: /healthz port: 80# 命令式告诉集群“现在去创建一个 Pod再创建一个 Service再创建一个 Ingress...” kubectl run nginx --imagenginx:1.21 --replicas3 kubectl expose deployment nginx --port80 --typeClusterIP kubectl create ingress nginx --rulenginx.example.com/*nginx:80两者的根本区别在于状态收敛能力。声明式配置下GitOps 控制器如 Argo CD会持续对比集群当前状态Actual State和 Git 中定义的目标状态Desired State。如果有人手动删除了一个 Pod控制器会在几秒内发现差异并自动重建如果有人kubectl scale deploy nginx --replicas5控制器会立刻把它缩回 3 个。这种“自愈”能力是命令式脚本永远无法提供的——脚本只执行一次执行完就结束它不会持续守护。我踩过的最深的坑是在一个混合云项目里误用了命令式思维。当时为了快速上线我们把helm install生成的 release manifest 直接kubectl get -o yaml manifest.yaml存入 Git。结果某天运维同学手动helm upgrade了 chart 版本Git 里的文件没更新。Argo CD 发现集群状态和 Git 不一致但它无法判断该“回滚到 Git 版本”还是“升级到集群版本”只能报错。后来我们彻底重构所有 Helm Release 都用 Helm Controller 管理Git 中只存HelmReleaseCRD它声明“我要安装 bitnami/nginx chart v10.0.0”具体渲染和部署由 Helm Controller 完成。这样Git 始终只存意图不存结果。2.3 GitOps 的两种落地模型Pull vs Push不是选择题而是演进路线很多文章把 Pull 和 Push 模型并列对比仿佛让你选一个。但在真实世界里它们是不同成熟度阶段的必然选择强行跳过 Push 直接上 Pull就像让新手不学自行车直接骑摩托。Push 模型GitHub Actions / GitLab CI的本质是“自动化脚本”。它的工作流是代码提交 → CI 流水线触发 → 执行kubectl apply -f infra/prod/→ 部署完成。它最大的优势是零学习成本如果你已经会写 GitHub Actions加几行 kubectl 命令就能跑起来。我们给初创团队做 PoC 时通常 2 小时就能搭好一套 Push GitOps让他们看到“改一行 YAML 就自动上线”的魔力。但它的致命缺陷是单向通道CI 流水线只知道“推”不知道“拉”。它无法感知集群是否被手动修改无法自动修复 drift也无法提供可视化状态视图。这就像你给家里装了个智能门锁但锁本身没有联网你只能用手机 App 发送“开锁”指令却不知道门是不是被别人用钥匙打开了。Pull 模型Argo CD / Flux的本质是“集群自治”。它在集群内部署一个 Operator这个 Operator 像个尽职的图书管理员24 小时盯着 Git 仓库。一旦发现新 commit它就去拉取、解析、计算差异、执行变更、等待健康检查、报告状态。最关键的是它会持续轮询即使没人提交代码它也会每 3 分钟检查一次集群当前状态是否匹配 Git 中的 Desired State。如果发现不匹配比如有人kubectl delete pod nginx-xxx它会立刻重建 Pod。这才是 GitOps 的“自愈”灵魂。我们团队的标准演进路径是第 1 周用 GitHub Actions 实现 Push 模型让所有成员习惯“配置即代码”建立基础 CI/CD 流水线第 2 月在预发环境部署 Argo CD将infra/staging/目录接入 Pull 模型让 SRE 团队体验 drift detection 和一键回滚第 3 季度生产环境全面切换到 Pull 模型并启用 Argo CD 的 ApplicationSet 功能实现“一个 PR 同时更新 10 个集群的配置”。注意Push 和 Pull 并非互斥。我们在大型项目中常用混合模式用 GitHub Actions 做 CI构建镜像、运行单元测试用 Argo CD 做 CD部署应用、管理配置。CI 流水线只负责把构建好的 Docker 镜像推送到 Registry并更新 Git 中的image: myapp:v1.2.3字段Argo CD 检测到这个变更自动拉取新镜像并滚动更新。这样既利用了 CI 的强大生态又保留了 Pull 的自愈能力。3. 核心细节解析与实操要点从 LLM 项目看 GitOps 的最小可行实践3.1 为什么 LLM 项目是 GitOps 的绝佳练兵场大语言模型应用LLM App天生具备 GitOps 的所有理想特征强依赖声明式配置一个 LLM 服务的性能70% 取决于resources.limits.memory、env.PYTORCH_CUDA_ALLOC_CONF、volumeMounts这些参数而非代码逻辑。这些参数必须精确、可复现、可版本化。环境差异巨大本地开发用 CPU 推理测试环境用 A10G生产环境用 A100不同 GPU 的 CUDA 版本、驱动、内存配额完全不同。GitOps 的environments/dev/和environments/prod/目录天然解决这个问题。安全敏感度高LLM 应用常需挂载 API Key、模型权重、敏感提示词模板。GitOps 的 Secret 管理如 SOPS Age 加密能确保这些密钥永不以明文形式出现在任何地方。我们以一个真实的 Gradio LLM Web UI 项目为例项目结构见输入内容展示如何用最简方式落地 GitOps。3.2 目录结构设计让 Git 成为你的“基础设施地图”一个反模式是把所有 YAML 堆在根目录下。正确的做法是按关注点分离Separation of Concerns设计my-llm-app/ ├── app/ # 应用代码层开发者关注 │ ├── main.py # Gradio UI 逻辑 │ ├── requirements.txt # Python 依赖含 transformers, torch │ └── Dockerfile # 构建镜像FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime ├── infra/ # 基础设施层SRE 关注 │ ├── base/ # 公共基线所有环境共享 │ │ ├── kustomization.yaml # 定义公共 label、namespace、common configmap │ │ └── deployment.yaml # 通用 Deployment 模板不含 env-specific 字段 │ ├── overlays/ # 环境覆盖层dev/staging/prod │ │ ├── dev/ │ │ │ ├── kustomization.yaml # 引用 base添加 dev-specific patch │ │ │ └── patch-cpu.yaml # 降低 resources.limits.cpu: 500m │ │ ├── staging/ │ │ └── production/ │ └── clusters/ # 集群专属配置如多云场景 │ ├── aws-eks/ │ └── gcp-gke/ ├── .github/workflows/ # CI/CD 层自动化引擎 │ ├── ci.yaml # 构建、测试、推送镜像 │ └── cd.yaml # 部署Push 模型或触发 Argo CDPull 模型 └── docs/ # 文档层所有角色关注 └── gitops-rules.md # 明确谁可以改哪个目录、PR 流程、回滚 SOP这个结构的关键在于infra/base/是“宪法”infra/overlays/*/是“法律解释”app/是“公民行为”。当你要给生产环境增加一个 Prometheus 监控 endpoint只需在infra/overlays/production/下加一个patch-metrics.yaml而不用动base/里的任何东西。这种设计让变更影响范围一目了然极大降低协作风险。3.3 Dockerfile 的 GitOps 化改造从“构建脚本”到“可验证制品”很多团队的 Dockerfile 写着RUN pip install -r requirements.txt这会导致每次构建都从 PyPI 拉包网络波动就失败且无法保证两次构建的依赖版本完全一致。GitOps 要求镜像构建是确定性Deterministic的。我们的改造方案是在app/目录下增加requirements.lock文件用pip-compile生成pip install pip-tools pip-compile requirements.inDockerfile 改为COPY requirements.lock . RUN pip install -r requirements.lockCI 流水线中增加一步pip-compile --upgrade requirements.in git add requirements.lock git commit -m chore(deps): update lockfile。这样requirements.lock就成了 Git 中的“依赖宪法”每次pip install的结果都可复现。我们曾用此方案定位一个诡异 bug开发在本地pip install了新版本 transformers但 CI 构建的镜像仍是旧版。通过对比requirements.lock的 Git 历史5 分钟就定位到是开发忘了提交 lock 文件。3.4 Kustomize 的深度应用用 patch 精准控制环境差异Kustomize 是 GitOps 的隐形功臣。它不像 Helm 那样需要学习模板语法而是用纯 YAML patch 操作学习成本极低且与 Git 天然契合。以 LLM 项目为例不同环境的典型差异开发环境需要--reload参数支持热重载resources.limits.memory: 2Gi生产环境需要--no-reloadresources.limits.memory: 32Gienv.TORCH_COMPILE: 1启用编译优化。传统做法是维护两套 Deployment YAML极易遗漏同步。Kustomize 方案如下# infra/overlays/dev/kustomization.yaml bases: - ../../base patches: - patch-cpu.yaml - patch-dev-args.yaml # 添加 --reload# infra/overlays/dev/patch-dev-args.yaml - op: add path: /spec/template/spec/containers/0/args value: [--reload]# infra/overlays/production/kustomization.yaml bases: - ../../base patches: - patch-prod-memory.yaml # 修改 memory limits - patch-prod-env.yaml # 添加 TORCH_COMPILE这样base/deployment.yaml只保留最精简的通用字段所有环境特有逻辑都在overlays/下。Git diff 时你只会看到patch-prod-memory.yaml的变更而不会被几百行重复的 YAML 淹没。3.5 GitHub Actions CI/CD 流水线从“能跑”到“可信”一个典型的ci.yaml流水线应包含以下关键环节按执行顺序步骤命令/工具为什么必须做GitOps 价值1. 代码扫描pylint app/,bandit -r app/防止硬编码 API Key、SQL 注入漏洞保障 Git 中代码的安全基线2. 依赖检查pip-compile --check requirements.in确保requirements.lock与requirements.in一致维护“依赖宪法”的权威性3. 构建镜像docker build -t ${{ secrets.REGISTRY }}/llm:${{ github.sha }} .生成唯一标识的制品为后续部署提供可追溯的制品 ID4. 镜像扫描trivy image --severity HIGH,CRITICAL ${{ secrets.REGISTRY }}/llm:${{ github.sha }}检测 CVE 漏洞防止带漏洞镜像进入生产环境5. 推送镜像docker push ${{ secrets.REGISTRY }}/llm:${{ github.sha }}将制品存入可信 Registry为 Pull 模型提供数据源注意cd.yaml的关键不是“怎么部署”而是“谁有权部署”。我们强制要求cd.yaml只能部署infra/overlays/production/目录下的配置必须开启concurrency: group: production-deploy, cancel-in-progress: true防止并发部署导致状态混乱所有kubectl apply命令必须加上--prune -l gitopsenabled自动清理 Git 中已删除的资源。4. 实操过程与核心环节实现手把手搭建 LLM 项目的 Push GitOps4.1 准备工作5 分钟搞定环境依赖在开始前请确保你有一个 GitHub 账号用于托管代码和 Secrets一个 Kubernetes 集群Minikube 本地测试足够或 EKS/GKEkubectl和helmCLI 已配置好Docker Desktop或podman已安装。关键安全步骤极易被跳过在 GitHub 仓库 Settings → Secrets and variables → Actions 中创建以下 SecretsKUBE_CONFIGBase64 编码的~/.kube/config文件仅限测试生产环境用 OIDCREGISTRYDocker Registry 地址如ghcr.io/yournameREGISTRY_USERNAME和REGISTRY_PASSWORDRegistry 凭据。在集群中创建专用 ServiceAccountkubectl create namespace llm-app kubectl create serviceaccount -n llm-app github-actions-sa kubectl create rolebinding -n llm-app github-actions-rb \ --clusterroleedit \ --serviceaccountllm-app:github-actions-sa提示KUBE_CONFIG用 Base64 是为了绕过 GitHub Secrets 对换行符的限制。实际使用时CI 流水线中用echo ${{ secrets.KUBE_CONFIG }} | base64 -d ~/.kube/config还原。4.2 编写ci.yaml构建、测试、推送一体化# .github/workflows/ci.yaml name: CI Pipeline on: push: paths: - app/** - infra/base/** - requirements.in - Dockerfile jobs: lint-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install dependencies run: | pip install pylint bandit pip-tools - name: Lint Python code run: pylint app/ - name: Security scan run: bandit -r app/ - name: Check requirements lock run: pip-compile --check requirements.in build-and-push: needs: lint-and-test runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up QEMU uses: docker/setup-qemu-actionv3 - name: Set up Docker Buildx uses: docker/setup-buildx-actionv3 - name: Login to Container Registry uses: docker/login-actionv3 with: registry: ${{ secrets.REGISTRY }} username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - name: Build and push uses: docker/build-push-actionv5 with: context: . push: true tags: ${{ secrets.REGISTRY }}/llm:${{ github.sha }} cache-from: typeregistry,ref${{ secrets.REGISTRY }}/llm:buildcache cache-to: typeregistry,ref${{ secrets.REGISTRY }}/llm:buildcache,modemax这个流水线的关键设计路径触发只在app/或requirements.in变更时运行避免无谓构建分阶段依赖lint-and-test成功后才执行build-and-push失败立即中断构建缓存cache-from/cache-to复用 layer将 10 分钟构建缩短到 90 秒安全扫描bandit检查app/中是否有os.system()调用防止命令注入。4.3 编写cd.yaml精准部署到指定环境# .github/workflows/cd.yaml name: CD Pipeline on: push: paths: - infra/overlays/production/** - infra/overlays/staging/** jobs: deploy-to-staging: if: github.event_name push startsWith(github.head_ref, staging) runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Configure Kubeconfig run: echo ${{ secrets.KUBE_CONFIG }} | base64 -d ~/.kube/config - name: Deploy to Staging run: | kubectl config use-context staging-cluster kubectl apply -k infra/overlays/staging/ --prune -l gitopsenabled deploy-to-production: if: github.event_name push startsWith(github.head_ref, production) runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Configure Kubeconfig run: echo ${{ secrets.KUBE_CONFIG }} | base64 -d ~/.kube/config - name: Deploy to Production run: | kubectl config use-context production-cluster kubectl apply -k infra/overlays/production/ --prune -l gitopsenabled这里有两个精妙设计环境隔离用if条件判断分支名确保staging分支的变更只影响 staging 环境标签驱逐--prune -l gitopsenabled要求所有部署的资源都打上gitops: enabledlabel这样当infra/overlays/staging/中删除一个 ConfigMap 时kubectl apply会自动删除集群中对应的资源避免“幽灵配置”残留。4.4 验证 GitOps 效果用三个命令看清一切部署完成后用以下命令验证 GitOps 是否真正生效查看当前部署状态kubectl get deploy -n llm-app nginx-llm -o wide # 输出应显示 IMAGE 为 ghcr.io/yourname/llm:abc123对应 Git commit SHA模拟人为 drift 并观察自愈仅 Push 模型需手动触发kubectl scale deploy -n llm-app nginx-llm --replicas1 kubectl get deploy -n llm-app nginx-llm # 显示 REPLICAS1 # 等待 2 分钟再次执行 kubectl get deploy -n llm-app nginx-llm # 显示 REPLICAS3因为 cd.yaml 每次 push 都会重置回滚到上一个版本git checkout HEAD~1 infra/overlays/production/ git commit -m revert: downgrade to v1.1.0 git push origin production # 2 分钟后kubectl get deploy -n llm-app nginx-llm 显示 IMAGE 已变回 v1.1.0注意真正的 Pull 模型Argo CD会自动检测 drift 并修复无需等待下次 push。但 Push 模型的“修复”发生在下一次代码提交时这是它与 Pull 的本质区别。5. 常见问题与排查技巧实录那些文档里不会写的血泪教训5.1 “镜像拉取失败”90% 的问题出在 Registry 权限现象Pod 一直处于ImagePullBackOff状态kubectl describe pod显示Failed to pull image ghcr.io/yourname/llm:abc123: rpc error: code Unknown desc failed to fetch anonymous token。排查路径检查集群节点是否能访问 Registrycurl -I https://ghcr.io/v2/应返回 200检查 Pod 的imagePullSecrets是否正确kubectl get secret -n llm-app github-pull-secret -o yaml最关键的一步检查cd.yaml中的kubectl apply是否在正确的 namespace 下执行。常见错误是忘记-n llm-app导致资源被部署到defaultnamespace而imagePullSecrets只在llm-appnamespace 有效。终极解决方案在infra/base/kustomization.yaml中统一定义secretGenerator: - name: github-pull-secret type: kubernetes.io/dockerconfigjson files: - .dockerconfigjson./secrets/dockerconfig.json这样所有环境都会自动注入正确的 Secret。5.2 “配置未生效”Kustomize patch 的隐式陷阱现象你在infra/overlays/production/patch-memory.yaml中修改了resources.limits.memory但kubectl get deploy -n llm-app nginx-llm -o yaml显示的仍是旧值。原因分析Kustomize 的 patch 顺序很重要。如果base/deployment.yaml中resources.limits.memory字段不存在而你的 patch 试图op: replace它会静默失败。必须用op: add或确保 base 中已存在该字段。调试命令# 查看 Kustomize 渲染后的最终 YAML在 CI 中加入此步骤 kustomize build infra/overlays/production/ rendered.yaml # 检查 rendered.yaml 中是否包含你的 patch 内容 grep -A5 memory rendered.yaml避坑技巧在base/deployment.yaml中预先定义所有可能被覆盖的字段即使值为空resources: limits: memory: cpu: requests: memory: cpu: 这样patch-memory.yaml就能安全地op: replace。5.3 “Secret 泄露风险”如何安全地管理 LLM 的 API Key这是 LLM 项目最危险的环节。绝不能把OPENAI_API_KEY明文写在infra/overlays/production/secrets.yaml里正确方案SOPS Age 加密安装sops和age生成 Age 密钥对age-keygen -o age.key将公钥存入 GitHub SecretsAGE_PUBLIC_KEY创建加密 Secretsops --encrypt --age ${{ secrets.AGE_PUBLIC_KEY }} secrets.yaml secrets.enc.yaml在cd.yaml中解密- name: Decrypt secrets run: | echo ${{ secrets.AGE_PRIVATE_KEY }} age.key sops --decrypt --age age.key secrets.enc.yaml secrets.yaml - name: Apply secrets run: kubectl apply -f secrets.yaml -n llm-app这样secrets.enc.yaml可以安全地存入 Git而私钥age.key永远只存在于 CI 运行时内存中。5.4 “GPU 资源不足”Kubernetes 调度器的隐藏规则现象Pod 一直处于Pending状态kubectl describe pod显示0/3 nodes are available: 3 Insufficient nvidia.com/gpu。根本原因Kubernetes 默认不识别 GPU 资源。你必须在每个 GPU 节点上安装 NVIDIA Device Pluginkubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.5/nvidia-device-plugin.yml在 Deployment 中显式请求 GPUresources: limits: nvidia.com/gpu: 1 # 请求 1 块 GPU requests: nvidia.com/gpu: 1经验技巧在infra/base/kustomization.yaml中添加一个gpu-labelerpatch自动给 GPU 节点打 label- op: add path: /spec/template/spec/nodeSelector value: {nvidia.com/gpu.present: true}这样所有使用 GPU 的应用都会被调度到打了nvidia.com/gpu.presenttruelabel 的节点上。5.5 “Gradio UI 无法访问”Ingress 配置的魔鬼细节现象kubectl get ingress显示ADDRESS为空或访问域名返回502 Bad Gateway。排查清单✅ 检查 Ingress Controller 是否运行kubectl get pods -n ingress-nginx✅ 检查 Ingress 资源是否引用了正确的 Servicekubectl get ingress -o wide中的BACKEND列✅最关键的一步检查 Service 的selector是否匹配 Pod 的labels。Gradio 默认的 Pod label 是app: gradio但你的 Deployment 可能写了app: nginx-llm导致 Ingress 找不到后端✅ 检查 TLS 证书如果启用了 HTTPS确保cert-manager已安装且Certificate资源处于Ready状态。速查命令# 查看 Ingress Controller 日志定位 502 原因 kubectl logs -n ingress-nginx deploy/ingress-nginx-controller | tail -20 # 检查 Service 是否有 Endpoints kubectl get endpoints -n llm-app nginx-llm-service # 如果 Endpoints 为空说明 Service selector 与 Pod labels 不匹配6. 从 Push 到 Pull平滑升级 Argo CD 的实战指南当你发现 Push 模型的局限