基于 K3s + GitOps 的云原生持续交付平台
技术栈 :K3s · Kubernetes · Helm · Kustomize · ArgoCD · Jenkins · Harbor · Trivy · Prometheus · Grafana · Alertmanager·Loki · Promtail
项目描述:独立设计并落地一套覆盖 CI 流水线、GitOps 部署、多环境发布管控、监控告警、弹性伸缩、权限管控的完整云原生体系,实现 dev 环境自动发布 + prod 环境手动卡点发布。
核心工作:
- 搭建双节点 K3s 集群,部署 Harbor 私有仓库、Ingress-Nginx、kube-prometheus-stack 监控栈;
- 设计 Kustomize base + overlays 多环境模型,差异化配置资源、HPA、PDB、Ingress,prod 通过
ignoreDifferences解决 selfHeal 与 HPA 副本数冲突问题; - 构建 Jenkins 声明式流水线(构建 → Trivy 高危阻断 → 推 Harbor → 自动更新 GitOps tag),push 后 5 分钟内完成 dev 自动部署;
- 落地 ArgoCD GitOps 拉模式发布:prune + selfHeal 配置漂移自愈、git revert 分钟级回滚、dev 自动 / prod 手动卡点;
- 自定义 PrometheusRule 三条业务告警 (副本归零 / CrashLoop / CPU 超 40%),配
for防抖动 + Alertmanager 邮件通知三级降噪 + 恢复闭环; - 搭建 Loki+Promtail 轻量日志体系,DaemonSet 采集容器日志并打标签,Grafana 结合 LogQL 检索、统计 Pod 错误速率;日志持久化,Pod 销毁后仍可查询。
- HPA 压测验证 3 → 10 副本自动扩缩容 ,RBAC 最小权限 + 24h 临时凭证 +
auth can-i四连验证。
故障排查亮点 :定位双层镜像缓存导致版本不更新 问题(Docker 构建缓存 + K8s IfNotPresent 拉取策略),通过 crictl images 在 K3s 节点定位旧镜像,落地 镜像 tag 不可变原则 + --no-cache 构建,彻底消除版本不一致。
项目成果 :部署效率 ≤5min ,故障发现 <3min ,回滚 分钟级 ,弹性伸缩 3→10 副本,Git 唯一可信源保证发布一致性。
===================
把"加机器搞 HA"的时间换成下面这些,面试加分密度高得多:
- Loki + Promtail 日志收集(半天)------补掉"只有监控没日志"这个更明显的短板,面试常问;(完成)
- Sealed Secrets 或 External Secrets (半天)------把
harbor-cred从手动创建变成 Git 管理,闭环 GitOps 密钥管理的最后一块; - Argo Rollouts 金丝雀发布(一天)------把 RollingUpdate 升级成 Canaries,简历多一个亮点词;
- 把现有项目的 README 补齐(半天)------架构图 + 截图 + 一段完整演示录屏,面试官点开仓库观感直接拉满。
四选一都比 HA 划算。优先级我推荐 1 → 2 → 4 → 3,因为日志和密钥管理是被问概率最高的两个"已知短板"。
结论:项目当前状态已经是能打的定稿,别再往基础设施上堆了。把精力放在上面 4 项中的任意一项,或者去刷八股------那才是决定 offer 的部分。
===========================
阶段0:前置规划
资源规划
| IP | 主机名 hostname | 角色 | 资源配置(CPU / 内存 / 存储) |
|---|---|---|---|
| 192.168.118.100 | k3s-server | k3s 控制面 + 工作节点 | 2 核 vCPU / 3GB 内存 / 100GB 系统盘 |
| 192.168.118.101 | k3s-agent | k3s agent 工作节点 | 2 核 vCPU / 3GB 内存 / 50GB 系统盘 |
| 192.168.118.102 | ci-jenkins | CI(Jenkins+Docker+Trivy) | 2 核 vCPU / 3GB 内存 / 100GB 数据盘(专门存 Docker 构建缓存) |
前置操作
🖥️第一台:192.168.118.100 k3s‑server
# 修改主机名
hostnamectl set-hostname k3s-server
# 设置静态IP,网卡名先确认:nmcli device 一般是ens33,如果你网卡名字不一样替换掉ens33
nmcli connection modify ens160 ipv4.addresses 192.168.118.100/24 ipv4.gateway 192.168.118.2 ipv4.dns "223.5.5.5,114.114.114.114" ipv4.method manual
# 重启网卡生效
nmcli connection up ens160
🖥️第二台:192.168.118.101 k3s‑agent
hostnamectl set-hostname k3s-agent
nmcli connection modify ens160 ipv4.addresses 192.168.118.101/24 ipv4.gateway 192.168.118.2 ipv4.dns "223.5.5.5,114.114.114.114" ipv4.method manual
nmcli connection up ens160
🖥️第三台:192.168.118.102 ci‑jenkins
hostnamectl set-hostname ci-jenkins
nmcli connection modify ens160 ipv4.addresses 192.168.118.102/24 ipv4.gateway 192.168.118.2 ipv4.dns "223.5.5.5,114.114.114.114" ipv4.method manual
nmcli connection up ens160
安装软件
yum install -y wget curl vim git tar bash-completion
三台机器都配置 /etc/hosts(所有节点都执行,做本地域名解析)
cat >> /etc/hosts <<EOF
192.168.118.100 k3s-server
192.168.118.101 k3s-agent
192.168.118.102 ci-jenkins
EOF
关闭 firewalld、selinux
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
systemctl stop firewalld
systemctl disable firewalld
关闭swap分区
swapoff -a
sed -i '/ swap / s/^/#/' /etc/fstab
free -h | grep -i swap # 期望 0B
时间同步
# ④ 时间同步
timedatectl set-timezone Asia/Shanghai
# CentOS: yum install -y chrony && systemctl enable --now chronyd
# Ubuntu: apt install -y chrony && systemctl enable --now chrony
timedatectl | grep -E 'Time zone|synchronized'
# 期望:Asia/Shanghai + NTP synchronized: yes
阶段 1:K3s Server 安装
# 在 Server 节点执行(国内镜像加速 + 禁用 Traefik + 写 kubeconfig 权限)
curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
INSTALL_K3S_MIRROR=cn \
INSTALL_K3S_EXEC="--disable=traefik --write-kubeconfig-mode 644" \
sh -
# 配置 kubectl(非 root 用户)
mkdir -p ~/.kube && sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config \
&& sudo chown $USER:$USER ~/.kube/config
阶段 2:加入K3s Agent 节点
# 在 Server 上获取 token
cat /var/lib/rancher/k3s/server/node-token
# 在 Agent 节点执行,token填入下面K3S_TOKEN位置
curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
INSTALL_K3S_MIRROR=cn \
K3S_URL=https://192.168.118.100:6443 \
K3S_TOKEN=K1027fba0cbc458d3182e0a3b5a6e435289665584d39dc828b1772e9df5d97e5e11::server:f57fb5a25e18c30a416ccc85006b1804 \
sh -
验证 :回到 Server 节点 kubectl get nodes,两个节点均 Ready

阶段 3:安装 Helm + Ingress-Nginx + Harbor
3.1 安装 Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
备选
# 备选:直接下载二进制
curl -fsSL https://mirrors.huaweicloud.com/helm/v3.16.3/helm-v3.16.3-linux-amd64.tar.gz -o helm.tgz
tar -zxvf helm.tgz && mv linux-amd64/helm /usr/local/bin/helm
3.2 安装 Ingress-Nginx(占用 80/443)
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
-n ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer
kubectl get svc -n ingress-nginx
3.3 部署 Harbor
kubectl create namespace harbor
helm repo add harbor https://helm.goharbor.io
helm repo update
cat > harbor-values.yaml <<EOF
expose:
type: nodePort
tls:
enabled: false
nodePort:
name: harbor
ports:
http:
port: 80
nodePort: 30002
externalURL: http://192.168.118.100:30002
harborAdminPassword: "Harbor12345"
EOF
helm install harbor harbor/harbor -n harbor -f harbor-values.yaml
kubectl get pods -n harbor # 全部 Running(约 2-3 分钟)
3.4 K3s 节点信任 Harbor HTTP 仓库
# 在 Server(192.168.118.100)和 Agent(192.168.118.101)两台机器上都执行:
mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/registries.yaml <<EOF
mirrors:
"192.168.118.100:30002":
endpoint:
- "http://192.168.118.100:30002"
EOF
sudo systemctl restart k3s # server 节点重启 k3s-server
sudo systemctl restart k3s-agent # agent 节点重启 k3s-agent
验证 :浏览器打开 http://192.168.118.100:30002,admin / Harbor12345 登录成功。

3.5 创建 imagePullSecret(应用侧认证,标准做法)
kubectl create namespace dev
kubectl create namespace prod
kubectl -n dev create secret docker-registry harbor-cred \
--docker-server=192.168.118.100:30002 \
--docker-username=admin \
--docker-password=Harbor12345
kubectl -n prod create secret docker-registry harbor-cred \
--docker-server=192.168.118.100:30002 \
--docker-username=admin \
--docker-password=Harbor12345
验证
kubectl -n dev get secret harbor-cred
kubectl -n prod get secret harbor-cred
后面 deployment.yaml 中引用 imagePullSecrets。
阶段 4:部署 ArgoCD
部署
kubectl create namespace argocd
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
cat > argocd-values.yaml <<EOF
server:
service:
type: NodePort
nodePortHttp: 30081
configs:
params:
server.insecure: "true"
EOF
helm install argocd argo/argo-cd -n argocd -f argocd-values.yaml
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d; echo
kubectl -n argocd get svc argocd-server
# values 已配了 NodePort 30081,PORTS 列应显示 80:30081/TCP
验证 :浏览器访问 http://192.168.118.100:30081,admin + 密码登录成功。

因为装的时候设了 server.insecure: "true",所以直接 http 访问,不用 https,浏览器也不会报证书错误。
阶段 5:部署 Prometheus + Grafana + Alertmanager
kubectl create namespace monitoring
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
cat > prom-values.yaml <<EOF
grafana:
adminPassword: "Grafana12345"
service:
type: NodePort
nodePort: 30090
alertmanager:
service:
type: NodePort
nodePort: 30093
EOF
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
-n monitoring -f prom-values.yaml
kubectl get pods -n monitoring # 全部 Running 约 3-5 分钟
验证 :Grafana http://192.168.118.100:30090,admin / Grafana12345,默认面板已有节点和 Pod 指标。

阶段 5.5:配置业务告警(PrometheusRule + 邮件通知)
5.5.1 创建业务告警规则
说明:kube-prometheus-stack 自带约 100 条基础设施默认规则(节点宕机、Pod CrashLoop 等),这里针对业务 demo-api 补 3 条自定义规则,体现"默认规则管基础设施、自定义规则管业务"的分层思路。
# 在 k3s-server 执行
cat > demo-api-alerts.yaml <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: demo-api-alerts
namespace: monitoring
labels:
release: kube-prometheus-stack # ⚠️ 必须写!Prometheus 按 release 标签挑选规则,写错规则永不加载
spec:
groups:
- name: demo-api.rules
rules:
# 规则1:业务 Deployment 无可用副本(触发方式最简单,用于验证链路)
- alert: DemoApiNoAvailableReplicas
expr: kube_deployment_status_replicas_available{namespace=~"dev|prod",deployment="demo-api"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "demo-api 无可用副本 ({{ $labels.namespace }})"
description: "命名空间 {{ $labels.namespace }} 的 demo-api 已持续 1 分钟无可用 Pod,服务中断"
# 规则2:Pod 频繁重启(dev/prod 命名空间内所有 Pod)
- alert: PodCrashLooping
expr: increase(kube_pod_container_status_restarts_total{namespace=~"dev|prod"}[15m]) > 3
for: 1m
labels:
severity: critical
annotations:
summary: "Pod 频繁重启 ({{ $labels.namespace }}/{{$labels.pod }})"
description: "Pod {{ $labels.pod }} 在 15 分钟内重启超过 3 次,疑似 CrashLoop"
# 规则3:demo-api CPU 超过 limit 的 40%(与 HPA 阈值对齐,压测时联动触发)
- alert: DemoApiHighCPU
expr: |
sum by (namespace, pod) (rate(container_cpu_usage_seconds_total{namespace="prod",pod=~"demo-api.*",container="demo-api"}[2m]))
/
sum by (namespace, pod) (kube_pod_container_resource_limits{namespace="prod",pod=~"demo-api.*",container="demo-api",resource="cpu"})
> 0.4
for: 2m
labels:
severity: warning
annotations:
summary: "demo-api CPU 过高 ({{ $labels.pod }})"
description: "Pod {{ $labels.pod }} CPU 使用率超过 limit 的 40% 持续 2 分钟,当前 {{$value | humanizePercentage }}"
EOF
kubectl apply -f demo-api-alerts.yaml
kubectl -n monitoring get prometheusrule
# 期望:demo-api-alerts 出现且 AGE 正常

http://192.168.118.100:9090/
#通过prometheus ui查看rules是否生效;需要开启port-forward

3 条规则的设计意图(面试可讲) :每条都配了 for 持续时间,防止瞬时抖动误报(比如 HPA 扩容瞬间 CPU 尖峰);CPU 阈值 40% 与 HPA 扩容阈值一致------HPA 负责自动扩容,告警负责让你知道"扩容发生了/可能扛不住",一个管自愈一个管知情。
5.5.2 配置邮件通知
⚠️ 关键背景:kube-prometheus-stack 默认所有告警路由到
null接收器------告警会在 Alertmanager UI 里显示,但不会发任何通知。不配 receiver,面试时"配了告警"的追问就露馅。
QQ 邮箱授权码获取:QQ邮箱网页版 → 设置 → 账号 → 开启 SMTP 服务 → 生成授权码。
# 编辑 prom-values.yaml,追加 alertmanager 配置段
cat > prom-values.yaml <<'EOF'
grafana:
adminPassword: "Grafana12345"
service:
type: NodePort
nodePort: 30090
alertmanager:
service:
type: NodePort
nodePort: 30093
config:
global:
resolve_timeout: 5m
smtp_smarthost: 'smtp.qq.com:587'
smtp_from: '2639721235@qq.com'
smtp_auth_username: '2639721235@qq.com'
smtp_auth_password: 'kzrvuwuvmtpbecia'
smtp_require_tls: true
route:
group_by: ['alertname', 'namespace']
group_wait: 30s # 首次通知前等 30s,把同组告警合并
group_interval: 5m
repeat_interval: 4h # 未恢复则每 4h 重复提醒
receiver: 'email-alert'
routes:
- receiver: 'null' # Watchdog 是默认心跳规则,静音掉
matchers:
- alertname = "Watchdog"
receivers:
- name: 'null'
- name: 'email-alert'
email_configs:
- to: '2639721235@qq.com'
send_resolved: true # 恢复时也发邮件,形成闭环
EOF
helm upgrade kube-prometheus-stack prometheus-community/kube-prometheus-stack \
-n monitoring -f prom-values.yaml
kubectl -n monitoring get pods -w # 等 alertmanager pod 滚动重启完成
5.5.3 验证是否生效(3 个触发实验)
实验 0:确认规则被 Prometheus 加载
# 本机端口转发查看 Prometheus 规则页(Prometheus UI 没暴露 NodePort)
kubectl -n monitoring port-forward svc/kube-prometheus-stack-prometheus 9090:9090 &
# 浏览器打开 http://localhost:9090/rules → 搜 demo-api
# 期望:3 条规则都在,State 为 inactive(还没触发)
如果 /rules 页搜不到 → 99% 是 release: kube-prometheus-stack 标签没写对。
实验 1:无可用副本告警(暂时无法检测,因为pod还没有开始部署)
kubectl -n dev scale deploy demo-api --replicas=0
#这里还有点问题 因为selfheal 5s就会自愈所以无法实现for一分钟,可以选择一个错误的镜像pod可以实现
- 浏览器开
http://192.168.118.100:30093(Alertmanager UI)→ 能看到 DemoApiNoAvailableReplicas 红色 Firing 条目,描述里带命名空间 → 截图 - QQ 邮箱收到告警邮件 → 截图
- 等约 3 分钟,ArgoCD selfHeal 会自动把副本拉回 1 → 告警自动变绿色 Resolved,邮箱收到恢复邮件(
send_resolved: true的效果)→ 又一张"自愈闭环"截图
验证告警规则 ✅、验证邮件通知 ✅、顺带演示了 ArgoCD 自愈和告警恢复联动 ✅



实验 2:CrashLoop 告警(暂时无法检测,因为pod还没有开始部署)
# 造一个必然崩溃的 Pod
kubectl -n dev run crasher --image=busybox:1.36 --restart=Always -- sh -c "exit 1"
# 2~3 分钟后 Alertmanager UI 出现 PodCrashLooping Firing
# 清理
kubectl -n dev delete pod crasher

实验 3:CPU 告警(和阶段 8 压测联动做)
# 直接复用阶段 8.2 的压测
kubectl -n prod run loadgen --image=httpd:alpine --restart=Never -- \
ab -n 999999999 -c 20 http://demo-api:8080/
kubectl -n prod get hpa -w # 一个终端看扩容
# 另一个终端:Alertmanager UI 大约 3~4 分钟后出现 DemoApiHighCPU
# 邮箱收到 warning 邮件
# 停压
kubectl -n prod delete pod loadgen
# 约 5 分钟后(HPA 缩容窗口)告警 Resolved
阶段 6:Kustomize 多环境配置(GitOps 仓库)
仓库结构:
gitops-config/
├── apps
│ └── demo-api
│ ├── base
│ │ ├── configmap.yaml
│ │ ├── deployment.yaml
│ │ ├── ingress.yaml
│ │ ├── kustomization.yaml
│ │ └── service.yaml
│ └── overlays
│ ├── dev
│ │ ├── kustomization.yaml
│ │ ├── patch-configmap.yaml
│ │ ├── patch-ingress.yaml
│ │ └── patch-resources.yaml
│ └── prod
│ ├── hpa.yaml
│ ├── kustomization.yaml
│ ├── patch-configmap.yaml
│ ├── patch-ingress.yaml
│ ├── patch-resources.yaml
│ └── pdb.yaml
└── argocd
├── dev-app.yaml
└── prod-app.yaml
本地创建目录的 shell 命令(在 gitops-config 根执行,一键生成全部空文件)
mkdir gitops-config
cd gitops-config
mkdir -p apps/demo-api/base
mkdir -p apps/demo-api/overlays/dev
mkdir -p apps/demo-api/overlays/prod
mkdir -p argocd
# === 创建全部空白yaml文件 ===
touch apps/demo-api/base/kustomization.yaml
touch apps/demo-api/base/deployment.yaml
touch apps/demo-api/base/service.yaml
touch apps/demo-api/base/configmap.yaml
touch apps/demo-api/base/ingress.yaml
touch apps/demo-api/overlays/dev/kustomization.yaml
touch apps/demo-api/overlays/dev/patch-resources.yaml
touch apps/demo-api/overlays/dev/patch-ingress.yaml
touch apps/demo-api/overlays/dev/patch-configmap.yaml
touch apps/demo-api/overlays/prod/kustomization.yaml
touch apps/demo-api/overlays/prod/patch-resources.yaml
touch apps/demo-api/overlays/prod/patch-ingress.yaml
touch apps/demo-api/overlays/prod/patch-configmap.yaml
touch apps/demo-api/overlays/prod/hpa.yaml
touch apps/demo-api/overlays/prod/pdb.yaml
touch argocd/dev-app.yaml
touch argocd/prod-app.yaml
base/deployment.yaml(含 imagePullSecrets):
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
labels:
app: demo-api
spec:
replicas: 2
selector:
matchLabels: { app: demo-api }
template:
metadata:
labels: { app: demo-api }
spec:
imagePullSecrets: # ⚠️ 拉取私有镜像必需
- name: harbor-cred
containers:
- name: demo-api
image: 192.168.118.100:30002/library/demo-api:v1.0.0
envFrom:
- configMapRef:
name: demo-api-config
ports:
- containerPort: 8080
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 512Mi }
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 10
readinessProbe:
httpGet: { path: /ready, port: 8080 }
initialDelaySeconds: 5
base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-api
spec:
selector:
app: demo-api
ports:
- name: http
port: 8080 # Service 端口,Ingress 指向这里
targetPort: 8080 # 容器端口
base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-api-config
data:
APP_ENV: "base" # 各环境 overlay 里覆盖
LOG_LEVEL: "info"
base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-api
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-api
port:
number: 8080
base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
- ingress.yaml
commonLabels:
app.kubernetes.io/name: demo-api
overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
- ../../base
images:
- name: 192.168.118.100:30002/library/demo-api
newTag: v1.0.0 # Jenkins 自动改写这里(仅 dev)
patches:
- path: patch-resources.yaml
- path: patch-ingress.yaml
- path: patch-configmap.yaml
overlays/dev/patch-resources.yaml(dev 用最小资源配置)
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
spec:
replicas: 1
template:
spec:
containers:
- name: demo-api
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 250m, memory: 256Mi }
overlays/dev/patch-ingress.yaml(⚠️ 解决双环境路由冲突)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-api
spec:
rules:
- host: dev.demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-api
port:
number: 8080
原理:Ingress 的
rules是无 merge key 的列表,strategic merge patch 会整体替换,所以这里写完整 rule。
overlays/dev/patch-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-api-config
data:
APP_ENV: dev
overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
- ../../base
- hpa.yaml
- pdb.yaml
images:
- name: 192.168.118.100:30002/library/demo-api
newTag: v1.0.0 # dev 验证通过后手动改这里发布 prod
patches:
- path: patch-resources.yaml
- path: patch-ingress.yaml
- path: patch-configmap.yaml
overlays/prod/patch-resources.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
spec:
replicas: 3 # 首次部署生效,之后由 HPA 接管(配合 ignoreDifferences)
template:
spec:
containers:
- name: demo-api
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: "1", memory: 1Gi }
overlays/prod/patch-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demo-api
spec:
rules:
- host: prod.demo.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demo-api
port:
number: 8080
overlays/prod/patch-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: demo-api-config
data:
APP_ENV: prod
overlays/prod/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: demo-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: demo-api
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 40
behavior:
scaleUp:
stabilizationWindowSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300
overlays/prod/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: demo-api
spec:
minAvailable: 2
selector:
matchLabels:
app: demo-api
argocd/dev-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo-api-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/pan20020422/gitops-config.git
targetRevision: main
path: apps/demo-api/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
ignoreDifferences:
- group: ""
kind: Secret
name: harbor-cred
jsonPointers:
- /data
argocd/prod-app.yaml (⚠️ 关键差异:ignoreDifferences)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo-api-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/pan20020422/gitops-config.git
targetRevision: main
path: apps/demo-api/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
ignoreDifferences: # ⚠️ 忽略 HPA 管理的副本数字段,避免与 selfHeal 打架
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
# 保护手动创建的harbor镜像拉取secret,不要被prune删除
- group: ""
kind: Secret
name: harbor-cred
jsonPointers:
- /data
本地渲染验证:
kubectl kustomize apps/demo-api/overlays/dev
kubectl kustomize apps/demo-api/overlays/prod
kubectl kustomize apps/demo-api/base
阶段 6.5:GitOps 冷启动引导
在ci-jenkins机器上安装docker
## 1. 卸载旧版本(podman、旧 docker)
```
dnf remove -y docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine \
podman runc
```
## 2. 安装 dnf 工具包,添加 docker 官方 yum 源
```
dnf install -y dnf-plugins-core
# 添加docker官方源
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
```
## 3. 安装 docker‑ce(引擎)、cli、containerd
```
dnf install -y docker-ce docker-ce-cli containerd.io
```
## 4. 启动 & 设置开机自启
```
systemctl start docker
systemctl enable docker
```
## 5. 验证 docker 是否安装成功
```
docker --version
docker run hello-world
```
## 6. (可选)非 root 用户免 sudo 执行 docker
```
usermod -aG docker $USER
# 重新登录ssh会话生效!
```
## 7.(可选)配置国内镜像加速 `/etc/docker/daemon.json`
```
mkdir -p /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
"insecure-registries": ["192.168.118.100:30002"],
"registry-mirrors": [
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com",
"https://docker.m.daocloud.io",
"https://docker.imgdb.de",
"https://docker-0.unsee.tech",
"https://docker.hlmirror.com"
]
}
EOF
# 重载重启docker
systemctl daemon-reload
systemctl restart docker
```
步骤 1:在装有 Docker 的机器上,建目录写两个文件
mkdir -p demo-api && cd demo-api
cat > app.py <<EOF
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/cpu":
# cpu压力:浮点循环运算
s = 0.0
for i in range(6_000_000):
s += (i ** 0.5)
self.send_response(200)
self.end_headers()
self.wfile.write(f"cpu burn done sum={s}\n".encode())
else:
self.send_response(200)
self.end_headers()
self.wfile.write(b"demo-api ok, path=" + self.path.encode())
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
EOF
cat > Dockerfile <<'EOF'
FROM python:3.12-alpine
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]
EOF
步骤 2:构建镜像
docker build -t 192.168.118.100:30002/library/demo-api:v1.0.0 .
看到 naming to ... done 即成功。
步骤2.5:信任harbor仓库
如果 Harbor 是 http(不是 https ),docker 默认拒绝访问 http 私有仓库! 必须配置 /etc/docker/daemon.json 增加 insecure‑registries
{
"insecure-registries":["192.168.118.100:30002"]
}
systemctl daemon-reload
systemctl restart docker
步骤 3:登录 Harbor
docker login 192.168.118.100:30002 -u admin -p Harbor12345
看到 Login Succeeded。
步骤 4:推送镜像
docker push 192.168.118.100:30002/library/demo-api:v1.0.0
最后显示 v1.0.0: digest: sha256:... size: xxxx 即成功。

步骤5:在github上创建公开仓库 gitops-config,并将本地文件夹gitops-config推送到github仓库
创建仓库

获取个人访问令牌
1. 登录 https://github.com,右上角头像 → `Settings`(设置)
2. 页面拉到底,左侧点 `Developer settings`(开发者设置)
3. `Personal access tokens` → `Tokens (classic)`
4. `Generate new token` → `Generate new token(classic)`
5. Note 备注:写 `k3s‑gitops‑jenkins`
6. Expiration(过期时间):学习可以选永不过期 `No expiration`
7. scope 权限:**只勾选 repo(全部仓库权限)**
8. 最下面点绿色按钮 `Generate token`
9. 复制生成出来的长 token 字符串,保存!关闭页面就丢失。


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
初始化本地git 仓库,并且推送gitops-config文件夹到github仓库
# 进入本地目录
cd gitops-config
git init
git branch -M main
# 设置远端github地址,替换成你自己github的https地址
git remote add origin https://github.com/pan20020422/gitops-config.git
git config --global user.email "2639721235@qq.com"
git config --global user.name "panlinfeng"
git add .
git commit -m "init: 提交kustomize gitops配置文件"
git push -u origin main


步骤6: 创建 Applications,验证 CD 链路
① 应用两个 Application (yaml 在阶段 6 的 argocd/ 目录里):
kubectl apply -f argocd/dev-app.yaml
kubectl apply -f argocd/prod-app.yaml
新建 Application 会立即触发首次同步(不用等轮询),约 1 分钟内完成拉取 → 渲染 → apply。
② 逐层验证:
# 第 1 层:App 状态 Synced + Healthy
kubectl -n argocd get applications
# 期望:demo-api-dev 和 demo-api-prod 均 Synced/Healthy
# 第 2 层:Pod 全部 Running 且 READY 1/1(探针通过的证据)
kubectl -n dev get pods
kubectl -n prod get pods
# 期望:dev 1 个副本,prod 3 个副本,均 Running 1/1
# 第 3 层:Service 有 Endpoint(readiness 通过才会注册)
kubectl -n dev get endpoints demo-api
# 期望:ENDPOINTS 有 Pod IP:8080
# 第 4 层:HPA 正常读取指标(prod)
kubectl -n prod get hpa
# 期望:TARGETS 显示 x%/70%(不再是 <unknown>)
③ 验证 ConfigMap 注入生效:
kubectl -n dev exec deploy/demo-api -- env | grep APP_ENV # 期望:APP_ENV=dev
kubectl -n prod exec deploy/demo-api -- env | grep APP_ENV # 期望:APP_ENV=prod
④ 验证外部访问(Ingress 按 host 分流):
curl -H "Host: dev.demo.local" http://192.168.118.100/;echo
curl -H "Host: prod.demo.local" http://192.168.118.100/;echo
# 期望:均返回 "demo-api ok, path=/"
步骤7:验证 GitOps 自愈能力
实验 1:K8s 自愈(秒级)------删 Pod 看重建:
kubectl -n dev delete pod -l app=demo-api
kubectl -n dev get pods -w # Deployment 控制器几秒内拉起新 Pod
实验 2:ArgoCD 自愈(分钟级)------手动改副本数看 Git 拉回:
kubectl -n dev scale deploy/demo-api --replicas=5
1. Git 仓库轮询(Refresh 仓库,默认 3min)
ArgoCD 每隔 3 分钟,主动去 Github/Gitlab 拉一次 yaml 文件,检查Git 里面的清单有没有改动。
这个是去访问外部 Git 仓库,慢,3 分钟一次。
作用:发现【Git 仓库 yaml 文件被人修改了】
2. Kubernetes 集群状态监听(集群实时事件,不用等 3 分钟,这里是这一种,由selfheal生效)
ArgoCD 会持续订阅 K8s APIServer 的事件,集群里面任何资源变化(比如你执行`kubectl scale`改副本),API 会立刻通知 ArgoCD,不需要轮询等待。
作用:发现【集群当前资源和 Git 里定义不一致,也就是漂移 Drift】
argocd app sync demo-api-dev
kubectl -n dev get deploy demo-api # 期望:又回到 1
注意:prod 不用做这个实验的 scale-up ------prod 有 HPA +
ignoreDifferences,副本数被 HPA 接管,ArgoCD 不会拉回,这是设计使然(dev 无 HPA,replicas 完全由 Git 决定,行为正好相反)。
阶段6.6:Loki + Promtail日志收集
Step 1:安装 loki-stack(约 5 分钟)
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
cat > loki-values.yaml <<'EOF'
loki:
enabled: true
persistence:
enabled: true
storageClassName: local-path # K3s 默认 StorageClass(Stage 2 装的)
size: 5Gi
promtail:
enabled: true
grafana:
enabled: false # 关键:复用监控栈的 Grafana,不再起第二个
EOF
helm install loki grafana/loki-stack -n monitoring -f loki-values.yaml
Step 2:验证组件拓扑
kubectl -n monitoring get pods -o wide | grep -E 'loki|promtail'
# 期望四行:
# loki-0 1/1 Running ← StatefulSet 单二进制,带 PVC
# loki-promtail-xxxxx 1/1 Running(在 k3s-server)
# loki-promtail-yyyyy 1/1 Running(在 k3s-agent)
# ------ DaemonSet:每个节点恰好一个,这就是"每节点采集器"模式的实证
kubectl -n monitoring get sts,ds,pvc
# loki(StatefulSet)+ loki-promtail(DaemonSet,DESIRED=2)+ storage 5Gi
对着架构图核对:单二进制 Loki(distributor/ingester/querier 三角色合一)+ 每节点一个 Promtail------小规模标准形态;生产规模会把三角色拆开 + 对象存储。
Step 3:声明式接入 Grafana(阶段 10 后执行,5 分钟)
不加任何 UI 操作,用 ConfigMap 声明数据源------kube-prometheus-stack 的 Grafana 自带 sidecar(一个监听 ConfigMap 的小容器),看到带特定标签的 ConfigMap 就自动加载:
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: loki-datasource
namespace: monitoring
labels:
grafana_datasource: "1"
data:
loki-datasource.yaml: |-
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki.monitoring.svc.cluster.local:3100
isDefault: false
#一个prometheus只有一个默认数据源
EOF
这里有个值得品的设计呼应 :数据源的接入也是"声明式"的------你声明期望状态(一个带标签的 ConfigMap),sidecar 控制循环把它变成现实。和 ArgoCD 看 Git、HPA 看指标是同一个模式。面试里把这条线讲出来("我项目里连 Grafana 数据源都是声明式管理的"),比"我点点 UI 加了个数据源"高一个段位。
验证:Grafana UI → Connections → Data sources,10 秒内出现 Loki,点 Save & test 显示绿色 ✓(URL 解析:svc名.命名空间.svc.cluster.local,集群内 DNS
Step 4:造日志 → 查日志(核心验收)
先造(两种方式):
# 方式 A:真实业务流量(http.server 每个 GET 都记日志)
curl -H "Host: dev.demo.local" http://192.168.118.100/
curl -H "Host: prod.demo.local" http://192.168.118.100/
# 方式 B:确定性测试行(保证有东西可查)
kubectl -n dev exec deploy/demo-api -- sh -c 'echo hello-loki-$(date +%s) > /dev/stderr'
# 原理:echo 写入 /dev/stderr → 落进容器日志文件 → Promtail 采集
后查------(Grafana Explore,推荐) :左侧菜单 → Explore → 数据源切 Loki → 标签选择器选 namespace = dev → Run query → 日志流出现,点开一条看完整标签(namespace/pod/container 全在------Promtail 通过 kubernetes 服务发现自动打的)。

Step 5:LogQL 三级跳(查询语言就是面试题)
在 Explore 依次试,每条理解一层:
# ① 标签选择 ------ 走索引,微秒级
{namespace="dev", container="demo-api"}
# ② 行过滤 ------ 命中后解压扫描,随日志量走(所以①要窄)
{namespace="dev", container="demo-api"} |= "GET" != "healthz"
# |= 包含 != 排除 |~ 正则(如 |~ "(?i)error|exception")
# ③ 指标聚合 ------ 日志当指标用,Loki 最出彩的能力
sum by (pod) (rate({namespace="dev", container="demo-api"}[5m])) # 每 Pod 每秒日志行数≈QPS 代理
sum by (pod) (count_over_time({namespace="dev"} |~ "error" [10m])) # 近10分钟每 Pod 错误行数
收口 :①索引查询、②③解压扫描------最佳实践永远是标签尽量窄 + 带时间范围。这正是标签设计要克制的原因。
阶段 7:Jenkins 部署(集群外 CI 机器)
步骤1:在 CI 机器上安装 Jenkins + Docker(已经安装) + Trivy
# 安装 JDK 21 + Jenkins
sudo dnf install -y wget
dnf install -y java-21-openjdk java-21-openjdk-devel
sudo wget -O /etc/yum.repos.d/jenkins.repo \
https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key
sudo dnf install -y jenkins
# 安装 Trivy
sudo tee /etc/yum.repos.d/trivy.repo << 'EOF'
[trivy]
name=Trivy repository
baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key
EOF
sudo dnf install -y trivy
trivy --version
#下载漏洞数据库
trivy image --download-db-only
systemctl enable --now jenkins
cat /var/lib/jenkins/secrets/initialAdminPassword # 初始密码
步骤2: Jenkins 初始化
浏览器 http://192.168.118.102:8080 → 输入初始密码 → 安装建议插件 → 创建管理员账号。

admin
Jenkins12345

步骤3: 配置凭证(Manage Jenkins → Credentials)
harbor-creds:Username/Password 类型,admin / Harbor12345git-creds:Username/Password 类型,pan20020422 / Personal Access Token
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

步骤4:Jenkinsfile(放业务代码仓库根目录)
必须写在 demo-api 目录下

Jenkinsfile内容
pipeline {
agent any
environment {
HARBOR = '192.168.118.100:30002'
IMAGE = "${HARBOR}/library/demo-api"
TAG = "v${BUILD_NUMBER}"
GITOPS_REPO = 'https://github.com/pan20020422/gitops-config.git'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build Image') {
steps {
sh "docker build -t ${IMAGE}:${TAG} ."
}
}
stage('Trivy Scan') {
steps {
sh "trivy image --exit-code 1 --severity HIGH,CRITICAL ${IMAGE}:${TAG}"
}
}
stage('Push Harbor') {
steps {
withCredentials([usernamePassword(credentialsId: 'harbor-creds',
usernameVariable: 'HUSER',
passwordVariable: 'HPASS')]) {
sh """
docker login ${HARBOR} -u ${HUSER} -p ${HPASS}
docker push ${IMAGE}:${TAG}
"""
}
}
}
stage('Update GitOps Repo') {
steps {
withCredentials([usernamePassword(credentialsId: 'git-creds',
usernameVariable: 'GUSER',
passwordVariable: 'GPASS')]) {
sh """
rm -rf gitops-config
git clone https://${GUSER}:${GPASS}@github.com/pan20020422/gitops-config.git
cd gitops-config
echo "======== sed 修改前 ========"
cat apps/demo-api/overlays/dev/kustomization.yaml
sed -i "s#newTag:[[:space:]]*.*#newTag: ${TAG}#" apps/demo-api/overlays/dev/kustomization.yaml
echo "======== sed 修改后 ========"
cat apps/demo-api/overlays/dev/kustomization.yaml
git config user.email "jenkins@ci.local"
git config user.name "jenkins-ci"
git add .
git commit -m "chore(dev): bump demo-api to ${TAG}"
git push origin main
"""
}
}
}
}
post {
always {
cleanWs()
}
}
}
步骤5:把业务仓库 demo-api 推到 GitHub
GitHub 上创建第二个 公开仓库 demo-api(和 gitops-config 并列),然后:

在jenkins机器
cd demo-api # app.py / Dockerfile / Jenkinsfile 所在目录
ls # 确认三个文件齐全:app.py Dockerfile Jenkinsfile
git init
git branch -M main
git remote add origin https://github.com/pan20020422/demo-api.git
git config --global user.email "2639721235@qq.com"
git config --global user.name "panlinfeng"
git add .
git commit -m "init: demo-api 应用代码 + Dockerfile + Jenkinsfile"
git push -u origin main
验证:GitHub 上 demo-api 仓库根目录有 3 个文件。
步骤6:jenkins 用户加入 docker 组
# 在 ci-jenkins 机器上
usermod -aG docker jenkins
systemctl restart jenkins
id jenkins | grep docker # 验证:输出里有 docker 组
步骤7:创建 Pipeline 任务
Jenkins 首页 → New Item → 名称 demo-api-pipeline → 选 Pipeline → OK。
配置页拉到最底部 Pipeline 区块:
| 字段 | 值 |
|---|---|
| Definition | Pipeline script from SCM |
| SCM | Git |
| Repository URL | https://github.com/pan20020422/demo-api.git |
| Credentials | 选择创建的github凭证 |
| Branch Specifier | */main |
| Script Path | Jenkinsfile |
Save。

步骤8:手动触发第一次构建
任务页 → Build Now → 点进构建 → Console Output。
第一次构建产出 v1(不是 v1.0.0),5 个 stage 依次变绿:
Checkout → Cloning repository... 完成
Build Image → naming to ...demo-api:v1 ... done
Trivy Scan → 首次下载漏洞库较慢,等待即可
Push Harbor → v1: digest: sha256:...
Update GitOps Repo → To https://github.com/...gitops-config.git main -> main
Finished: SUCCESS

步骤9:全链路验证(5 个观察点)
① Harbor 有新镜像:library/demo-api → 出现 v1

② gitops-config 有新 commit:GitHub 上看到 "chore(dev): bump demo-api to v1" 且 overlays/dev/kustomization.yaml 的 newTag 变成 v1


③ ArgoCD 自动同步 dev(prod 不动------环境隔离)
kubectl -n dev get deploy demo-api -o jsonpath='{.spec.template.spec.containers0.image}'; echo
期望:...demo-api:v1 (等待3分钟左右,argocd才会检测github仓库文件)
kubectl -n prod get deploy demo-api -o jsonpath='{.spec.template.spec.containers0.image}'; echo
期望:还是 v1.0.0

④ Pod 滚动更新完成
kubectl -n dev rollout status deploy/demo-api
kubectl -n dev get pods # Running 1/1

⑤ 业务可访问
curl -H "Host: dev.demo.local" http://192.168.118.100/;echo
期望:demo-api ok, path=/

步骤10:配置自动触发
任务 → Configure → Build Triggers → 勾 Poll SCM → Schedule 填:
H/2 * * * *
验证:改一行 app.py → push → 2 分钟内 Jenkins 自动构建 → 产出 v2 → ArgoCD 自动更新 dev。
阶段 8:HPA 弹性伸缩验证 + 压测
8.1 记录基线
kubectl -n prod get hpa
kubectl -n prod get pods -l app=demo-api
# 期望:TARGETS 为 x%/40%,REPLICAS 3
8.2 制造负载(单 Pod,20 并发持续压测)
kubectl -n prod run loadgen --image=httpd:alpine --restart=Never -- \
ab -n 999999999 -c 20 http://demo-api:8080/
8.3 观察自动扩容(另一个终端)
kubectl -n prod get hpa -w
# 期望:CPU 升到 40% 以上 → REPLICAS 从 3 → 5 → 8 ...(上限 10)
kubectl -n prod get pods -l app=demo-api -w
# 期望:新 Pod 陆续 Running
8.4 Grafana 观察曲线
浏览器 http://192.168.118.100:30090 → Dashboards → Kubernetes / Compute Resources / Pod → namespace 选 prod → 看 demo-api 的 CPU 曲线随压测抬升。截图。



8.5 告警
压测期间 Alertmanager 会同时触发 DemoApiHighCPU 告警------负载 → HPA 扩容 → 告警通知 → 负载停止 → 缩容 → 告警恢复
8.6 停压观察缩容
#停压
kubectl -n prod delete pod loadgen
kubectl -n prod get hpa -w
# 期望:scaleDown 稳定窗口 300 秒,约 5 分钟后 REPLICAS 逐步回落到 3


记录"3 → 峰值 → 3"的完整数据,这是简历 HPA bullet 的直接证据。
阶段 9:RBAC 权限管理
9.1 创建"开发团队"受限账号(只能操作 dev,不能删 Secret)
kubectl -n dev create serviceaccount dev-user
cat > dev-role.yaml <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: dev-team-role
namespace: dev
rules:
- apiGroups: ["", "apps", "networking.k8s.io"]
resources: ["pods", "pods/log", "deployments", "services", "configmaps", "ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: dev-team-binding
namespace: dev
subjects:
- kind: ServiceAccount
name: dev-user
namespace: dev
roleRef:
kind: Role
name: dev-team-role
apiGroup: rbac.authorization.k8s.io
EOF
kubectl apply -f dev-role.yaml
9.2 签发临时凭证(24 小时)
kubectl -n dev create token dev-user --duration=24h
9.3 权限验证(面试演示素材)
SA=system:serviceaccount:dev:dev-user
kubectl -n dev auth can-i get secrets --as=$SA # yes(Role 给了 get/list)
kubectl -n dev auth can-i get deployments --as=$SA # yes
kubectl -n dev auth can-i delete secrets --as=$SA # no
kubectl -n prod auth can-i get pods --as=$SA # no(跨命名空间被拒)
阶段 10:端到端发版演练 + 整体验收
10.1 模拟一次真实发版(dev 自动、prod 手动卡点)
# 1) 改业务代码
cd demo-api
sed -i 's/demo-api ok/demo-api v2 ok/' app.py
git add . && git commit -m "feat: v2 response" && git push origin main
# 2) 等 Poll SCM 触发构建(≤2 分钟),产出新 tag vN(N=第几次构建)
# 3) 验证环境隔离:dev 已更新,prod 未动
kubectl -n dev get deploy demo-api -o jsonpath='{.spec.template.spec.containers[0].image}'; echo # :vN
kubectl -n prod get deploy demo-api -o jsonpath='{.spec.template.spec.containers[0].image}'; echo # 旧版
curl -H "Host: dev.demo.local" http://192.168.118.100/ ;echo # demo-api v2 ok
# 4) dev 验证通过 → 手动提升 prod(卡点发布)
# 如果这台机器还没 clone 过,先:
git clone https://github.com/pan20020422/gitops-config.git
cd gitops-config
# 新增:先同步远端最新,用rebase拉直提交历史
git pull --rebase
sed -i "s#newTag: .*#newTag: vN#" apps/demo-api/overlays/prod/kustomization.yaml
git add . && git commit -m "release: promote demo-api to vN (prod)" && git push origin main
## 核心思想(卡点发布)
- dev:**流水线自动更新 tag,自动发布,开发迭代自动部署**
- prod:**禁止流水线自动修改!必须运维 / 开发人员手动本地改 gitops‑config,提交推送,人为确认之后才发布上线。**
实现了**开发环境自动,生产环境手动卡点审批发布**,防止代码未经测试直接跑到生产。
# 5) ArgoCD 自动滚动 prod
kubectl -n prod rollout status deploy/demo-api
curl -H "Host: prod.demo.local" http://192.168.118.100/ # demo-api v2 ok


pan20020422
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
访问argoui

10.2 回滚演练(GitOps 正确姿势)
# 反面教材:kubectl rollout undo 会被 selfHeal 拉回,可先演示一次这个"打架"
kubectl -n prod rollout undo deploy/demo-api
kubectl -n prod get deploy demo-api -o jsonpath='{.spec.template.spec.containers[0].image}'; echo
# 稍后被 ArgoCD 自动改回 ------ 证明 Git 才是唯一事实源
结果展示
[root@k3s-server ~]# kubectl -n prod get deploy demo-api -o jsonpath='{.spec.template.spec.containers[0].image}' -w
192.168.118.100:30002/library/demo-api:v6
192.168.118.100:30002/library/demo-api:v1.0.0
192.168.118.100:30002/library/demo-api:v1.0.0
192.168.118.100:30002/library/demo-api:v1.0.0
192.168.118.100:30002/library/demo-api:v1.0.0
192.168.118.100:30002/library/demo-api:v6
192.168.118.100:30002/library/demo-api:v6
192.168.118.100:30002/library/demo-api:v6
192.168.118.100:30002/library/demo-api:v6
# 正确姿势:revert Git
cd ~/gitops-config
git log --oneline -3
git revert <promote 那条 commit 的 hash>
git push origin main
kubectl -n prod rollout status deploy/demo-api
kubectl -n prod get deploy demo-api -o jsonpath='{.spec.template.spec.containers[0].image}'; echo
# 期望:回到旧 tag

# 先查看全部修订
kubectl -n prod rollout history deploy/demo-api
# 直接回退到指定revision号,例如回退到revision=2
kubectl -n prod rollout undo deploy/demo-api --to-revision=2
#回退上一个版本
kubectl -n prod rollout undo deploy/demo-api
ArgoCD 的 Application 资源,后台周期性拉取 Git 仓库,对比集群状态和 Git 期望状态。 每 180s 去 Git 拉一遍,如果发现 Git 有新 commit,就开始同步;同时 selfHeal 自愈也是这个周期检查集群和 Git 是否一致。
=================================
# GitOps 项目面试复盘:镜像缓存导致版本不更新问题
## 一、项目场景简述
我在基于 ArgoCD + Jenkins + GitOps 落地持续部署项目时,搭建了完整的 CI/CD 流水线:业务代码提交 GitHub 触发 Jenkins 构建镜像、推送到 Harbor 私有仓库,自动更新 GitOps 配置仓库的镜像 Tag,最终由 ArgoCD 自动同步更新 K8s 集群,实现 Dev 环境自动发布、Prod 环境手动卡点发布的流程。
## 二、项目遇到的核心问题
在联调完整流水线时,出现了代码更新成功、镜像构建推送成功、ArgoCD 同步显示成功,K8s pod ,deployment tag版本都更新了,但 K8s 业务服务始终访问内容没更新诡异问题,接口一直返回旧版本内容,无法生效新代码。
## 三、问题排查思路
1. **先排除业务代码问题**
查看git本地仓库、GitHub 远程仓库、Jenkins 拉取的 SCM 代码 Commit 记录(github仓库有commit hash),核对 commit 哈希,确认 Jenkins 已经拉取到最新提交`0d44320`,源码本身已经修改完成,排除源码提交遗漏的问题。
> 🔧我的实操:我先打开 github 网页确认 app.py 已经提交;再点开 Jenkins 构建日志看 checkout 检出的 commit id,和远端 commit 做比对,确认工作区拿到新版源码。
2. **排除 Jenkins 构建流程问题**
查看 Jenkins 完整流水线日志,确认流水线正常执行构建、镜像打包、推送 Harbor、自动提交更新 GitOps 配置仓库 newTag,每一步都返回成功,CI 流程完全正常。
> 🔧我的实操:在 Jenkins 控制台看每一段 sh 输出;登录 Harbor 网页界面,查看 v4 镜像的更新时间,确认镜像确实已经覆盖上传到私有仓库;再打开 gitops‑config 仓库,确认 yaml 里面 tag 值已经被流水线修改。
3. **排除 ArgoCD 同步问题**
ArgoCD UI 页面状态显示为 Synced、Healthy,成功识别 Git 镜像 Tag 变更;观察 K8s Deployment 事件,发现已经触发滚动更新,集群生成了全新 Pod,资源层面没有报错。
> 🔧我的实操:网页打开 ArgoCD 看应用状态;执行`kubectl get deploy`、`kubectl describe pod`查看 Pod 重建事件;进入新 Pod 里面`cat /app.py`查看容器内代码,发现 Pod 里面依旧是旧版本代码,此时意识到问题不在 Git 和 ArgoCD 同步上。
4. **定位最终根因:双重缓存问题(核心亮点)**
最终定位问题是双层缓存叠加导致版本不生效,也是 GitOps + K8s 最经典的生产坑:
K8s 默认镜像拉取策略为 `IfNotPresent`:只要本地存在对应 Tag 镜像,就优先使用本地缓存,不会去镜像仓库拉取最新覆盖后的镜像。
> 🔧我的实操:登录 k3s 集群节点,执行`crictl images | grep demo-api`,发现节点本地还留存旧的 v4 镜像;我临时手动执行`crictl rmi 192.168.118.100:30002/library/demo-api:v4`删除本地缓存镜像;删除旧 Pod 让它重新拉起,新版本代码终于生效,复现和验证完根因。
## 四、最终解决方案
1. **紧急临时解决(测试环境)**
清理 K8s 节点本地旧镜像缓存,或修改镜像拉取策略为 Always,强制每次启动 Pod 都拉取最新镜像,临时解决版本不更新问题。
2. **规范根治方案(生产最佳实践,重点说)**
构建命令增加 `--no-cache`,保证每次构建都是完整全新镜像,杜绝构建层缓存导致代码不更新;**业务发布不再复用旧 tag,每次迭代生成全新版本 tag(v5 或者 git commit 短哈希),修改 gitops‑config 仓库的 newTag,交由 ArgoCD 完成滚动发布。**
> 🔧我的实操:修改 Jenkinsfile 构建脚本,加上`--no-cache`参数;后续流水线改成使用 git commit 短哈希作为镜像标签;不再做镜像覆盖;提交测试之后观察 Pod,K8s 本地没有该 tag 镜像,会主动从 Harbor 拉取镜像,新版本自动生效。
## 五、项目总结 & 技术收获(面试加分升华)
通过这个问题我深刻理解了 GitOps 的核心思想:**Git 是唯一可信源,镜像 Tag 必须不可变**。
同时掌握了 K8s 镜像拉取策略的底层机制,区分了 Docker 构建缓存 和 K8s 节点镜像缓存 两层不同缓存问题,解决了企业级 CI/CD 流水线最常见的版本不一致问题。
后续在生产环境落地中,我统一规范了**镜像版本不可复用、流水线无缓存构建、版本递进发布**的标准流程,保证部署一致性。
---
### 🎤1 分钟口述精简版(面试口头回答用)
>
> 排查的时候我是一步步来的。首先核对 Github 源码和 Jenkins 检出的 commit 哈希,确认源码没问题;接着看 Jenkins 日志、登录 Harbor 确认镜像推送成功,也确认 gitops 配置库 tag 已经更新;然后看 ArgoCD 已经同步完成,Deployment 也新建了 Pod。
> 但是进到 Pod 里面看文件发现代码还是旧的。我登录 k3s 节点执行 crictl images,发现节点本地存了旧 v4 镜像。删掉本地镜像之后重建 Pod 版本就正常了,定位到是两层缓存问题。
> 后面根治我改了 Jenkinsfile,构建带上 --no‑cache,并且落实镜像 tag 不可变,每次发布使用全新 tag,不再覆盖旧镜像,彻底解决这个版本不一致的问题。