基于 K3s 的微服务容器化与 GitOps 持续交付平台

基于 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"的时间换成下面这些,面试加分密度高得多:

  1. Loki + Promtail 日志收集(半天)------补掉"只有监控没日志"这个更明显的短板,面试常问;(完成)
  2. Sealed Secrets 或 External Secrets (半天)------把 harbor-cred 从手动创建变成 Git 管理,闭环 GitOps 密钥管理的最后一块;
  3. Argo Rollouts 金丝雀发布(一天)------把 RollingUpdate 升级成 Canaries,简历多一个亮点词;
  4. 把现有项目的 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 / Harbor12345
  • git-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,不再覆盖旧镜像,彻底解决这个版本不一致的问题。
相关推荐
张彦峰ZYF1 小时前
一次触发、一把锁与一个号码:分布式基础组件的统一设计推演
架构·先立六条时间纪律·再拆解编号的位分配·节点编号分配与回拨四策·辨析互斥、租约与领导权三种语·调度拆成七个子问题·五层架构
Dawson Zhu2 小时前
《Agentic Design Patterns》第 17 章导读:推理技术
人工智能·语言模型·架构·aigc·agi
张洛闻Eren2 小时前
Ansible【第二课】:实施 Ansible Playbook
linux·运维·服务器·云原生·kubernetes·k8s·ansible
做运维的阿瑞2 小时前
Terraform 入门实战:安装与实施
云原生·云计算·terraform
各类产品分享12 小时前
制药厂 GMP 车间 AR 巡检系统落地架构与实施要点
架构·ar·ar巡检
云和恩墨2 小时前
从通用规范到系统级联动:以Skill为核心的Mopheus扩展架构与落地实践
架构
行者全栈架构师3 小时前
给开源 Rust 工具加一个新检查项,TRAE 帮我从想法跑到构建产物:全链路实战
前端·算法·架构
kevien_G13 小时前
微服务之OpenFeign最佳实践
java·微服务·架构
forest_093 小时前
Agent 系列07|记忆与状态设计
人工智能·机器学习·架构·prompt·agi