Appearance
GitOps 从零上手:容器镜像 → K8s 部署 → Flux CD 工作流(完整实操笔记)
第一章《从零上手 GitOps》完整实操记录。路线:构建容器镜像 → 部署到 Kubernetes(含自愈与自动扩缩容实测)→ 用 Flux CD 搭起「以 Git 为唯一事实来源」的 GitOps 工作流。本文保留全部命令、清单文件与演示输出,方便照着敲。
1. 构建容器镜像
1.1 先跑一个现成的镜像
bash
docker pull lyzhang1999/hello-world-flask:latest
docker images
docker run -d -p 8000:5000 lyzhang1999/hello-world-flask:latest
docker ps
docker exec -it <容器ID> bash1.2 写自己的 Flask 应用
app.py:返回一句话 + 容器 hostname(后面验证负载均衡/自愈全靠它区分是哪个 Pod 在响应):
python
from flask import Flask
import os
app = Flask(__name__)
app.run(debug=True)
@app.route('/')
def hello_world():
return 'Hello, my first docker images!' + os.getenv('HOSTNAME') + ''requirements.txt:⚠️ 必须锁版本。Flask 2.2.2 要配 Werkzeug 2.2.3——Werkzeug 3.0 移除了 url_quote,不锁版本会装到 3.x 导致 ImportError:
txt
Flask==2.2.2
Werkzeug==2.2.3Dockerfile:python:3.8-slim-bookworm 基础镜像;装上 procps(提供 killall,后面自愈测试要用)、vim、apache2-utils(提供 ab,压测要用)等调试工具:
dockerfile
FROM python:3.8-slim-bookworm
RUN apt-get update && apt-get install -y procps vim apache2-utils && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0"]构建并运行:
bash
docker build -t hello-world-flask .
docker images
docker run -d -p 8000:5000 hello-world-flask:latest1.3 发布到镜像仓库
bash
docker login
docker tag hello-world-flask robotzsj/hello-world-flask
docker push robotzsj/hello-world-flask2. 部署到 Kubernetes
2.1 给集群添加镜像仓库凭据(拉私有镜像)
bash
kubectl create secret docker-registry regcred \
--docker-server=docker.io \
--docker-username=<你的用户名> \
--docker-password=<你的密码> \
--docker-email=<你的邮箱>注意:密码属于敏感信息,不要写进公开仓库或博客。
2.2 最小 Pod 清单
yaml
apiVersion: v1
kind: Pod
metadata:
name: hello-world-flask
spec:
containers:
- name: flask
image: robotzsj/hello-world-flask:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000临时访问:端口转发 + 放行防火墙:
bash
sudo ufw allow 8001/tcp
kubectl port-forward pod/hello-world-flask 8001:5000 --address 0.0.0.02.3 从 Pod 升级到 Deployment + Service + Ingress
Deployment(副本数 2,这是自愈与负载均衡的基础):
bash
kubectl create deployment hello-world-flask --image=lyzhang1999/hello-world-flask:latest --replicas=2
# 编辑
kubectl edit deploy hello-world-flask
# 先预览再创建(dry-run 输出 yaml 供检查)
kubectl create deployment hello-world-flask --image=lyzhang1999/hello-world-flask:latest --replicas=2 --dry-run=client -oyamlService(ClusterIP,集群内访问入口):
bash
kubectl create service clusterip hello-world-flask --tcp=5000:5000Ingress(对外访问):注意 k3s 自带 traefik,所以 ingressClassName 用 traefik 而不是 nginx:
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello-world-flask
namespace: default
spec:
ingressClassName: traefik
rules:
- http:
paths:
- backend:
service:
name: hello-world-flask
port:
number: 5000
path: /
pathType: Exact配好后直接访问(无需端口转发)。如果集群不是 k3s、需要 nginx ingress,可以换装 ingress-nginx(Helm 方式):
bash
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
kubectl create namespace ingress-nginx
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer2.4 自愈实测
测试脚本:每秒 curl 一次,观察响应变化:
bash
while true; do sleep 1; curl http://127.0.0.1; echo -e '\n'$(date); done模拟故障:杀掉 Pod 里的应用进程:
bash
kubectl exec -it hello-world-flask-<pod> -- bash -c "killall python3"观察到的现象(关键在 hostname 与一次 Bad Gateway):
18:26:05 CST Hello, my first docker images! hello-world-flask-7b9677d578-wz6cw
18:26:06 CST Hello, my first docker images! hello-world-flask-7b9677d578-rw2wq
18:26:07 CST Bad Gateway ← 被杀的 Pod 恰好响应,出现瞬时错误
18:26:08 CST Hello, my first docker images! hello-world-flask-7b9677d578-wz6cw
18:26:09 CST Hello, my first docker images! hello-world-flask-7b9677d578-wz6cw
18:26:10 CST Hello, my first docker images! hello-world-flask-7b9677d578-rw2wq
...结论:杀掉一个 Pod 里的进程后,Deployment 自动重建了它(新 Pod),服务在短暂 Bad Gateway 后自动恢复——这就是 Kubernetes 自愈。
3. 自动扩缩容(HPA)
HPA 依赖 metrics-server 提供 CPU 等监控指标。
1. 安装 metrics-server:
bash
kubectl apply -f https://raw.githubusercontent.com/lyzhang1999/resource/main/metrics/metrics.yaml
# 等待就绪
kubectl wait deployment -n kube-system metrics-server --for condition=Available=True --timeout=90s
kubectl get pods -n kube-system -l k8s-app=metrics-server2. 创建扩缩容策略(CPU 超 50% 自动扩容,副本 2~10):
bash
kubectl autoscale deployment hello-world-flask --cpu-percent=50 --min=2 --max=103. 设置资源配额:⚠️ 不给 Deployment 设 resources.requests,HPA 不生效:
bash
# 方式一:patch
kubectl patch deployment hello-world-flask --type='json' -p='[{"op": "add", "path": "spec/template/spec/containers/0/resources", "value": {"requests":{"memory": "100Mi", "cpu": "100m"}}}]'
# 方式二(k3s 也可用):set resources
kubectl set resources deployment hello-world-flask -c=hello-world-flask --requests=cpu=100m,memory=100Mi
# 方式三:merge patch
kubectl patch deployment hello-world-flask --type=merge -p='{"spec":{"template":{"spec":{"containers":[{"name":"hello-world-flask","resources":{"requests":{"cpu":"100m","memory":"100Mi"}}}}]}}}}'4. 压测模拟高峰:
bash
kubectl exec -it hello-world-flask-<pod> -- bash
ab -c 50 -n 10000 http://127.0.0.1:5000/5. 观察扩容(kubectl get pod -w):副本从 2 个一路涨到多个(Pending → ContainerCreating → Running):
hello-world-flask-7c5c949dd9-5l8x8 1/1 Running
hello-world-flask-7c5c949dd9-mgdhh 1/1 Running
hello-world-flask-7c5c949dd9-rl4mr 0/1 Pending
hello-world-flask-7c5c949dd9-r77l6 0/1 ContainerCreating
...(多个新 Pod 陆续创建)
hello-world-flask-7c5c949dd9-rl4mr 1/1 Running
hello-world-flask-7c5c949dd9-r77l6 1/1 Running4. GitOps 工作流(Flux CD)
4.1 传统发布方式的三种更新
kubectl set image直接改运行中的应用- 修改本地 manifest 再
kubectl apply kubectl edit改集群内的 manifest
共同缺点:手动、不可审计、集群状态容易漂移。GitOps 把这一切换成「改 Git → 自动同步」。
4.2 安装 Flux CD
bash
kubectl apply -f https://raw.githubusercontent.com/lyzhang1999/resource/main/fluxcd/fluxcd.yaml4.3 构建 GitOps 工作流
① 准备仓库:建一个目录,把 Deployment 清单放进 git 仓库(这里只放 deployment.yaml):
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: hello-world-flask
name: hello-world-flask
spec:
replicas: 2
selector:
matchLabels:
app: hello-world-flask
template:
metadata:
labels:
app: hello-world-flask
spec:
containers:
- image: docker.1ms.run/lyzhang1999/hello-world-flask:v1
imagePullPolicy: IfNotPresent
name: hello-world-flaskbash
git init
git add -A && git commit -m "Add deployment"
git branch -M main
git remote add origin https://github.com/<你的账号>/fluxcd-demo.git
git push -u origin main② 创建 GitRepository(声明"从哪个仓库拉"):
yaml
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: GitRepository
metadata:
name: hello-world-flask
spec:
interval: 5s
ref:
branch: master
url: https://github.com/<你的账号>/fluxcd-demo.gitbash
kubectl apply -f fluxcd-repo.yaml
kubectl get gitrepository
# NAME URL AGE READY STATUS
# hello-world-flask https://github.com/xxx/fluxcd-demo 10s True stored artifact for revision 'master/<sha>'③ 创建 Kustomization(声明"把清单 apply 到集群"):
yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: hello-world-flask
spec:
interval: 5s
path: ./
prune: true
sourceRef:
kind: GitRepository
name: hello-world-flask
targetNamespace: defaultbash
kubectl apply -f fluxcd-kustomize.yaml
kubectl get kustomizationFlux 每 5s 对比一次工作负载差异:识别到期望状态和集群实际状态存在差异时,触发重新部署。
4.4 自动发布
修改仓库里的 deployment.yaml 镜像 tag → 推送:
bash
git add -A && git commit -m "Update image tag to v1"
git push origin masterFlux 自动应用;用下面命令查看触发重新部署的事件:
bash
kubectl describe kustomization hello-world-flask4.5 快速回滚
Git 即回滚:
bash
git reset --hard <commit>
git push origin main -f4.6 整体链路
开发者 / 我
│ git push(提交到 master)
▼
GitHub 公共仓库 zsjlu/fluxcd-demo
▲ │ ① 每 5s 轮询(interval: 5s)
│ ▼
│ ┌───────────────────────────────┐
│ ② 拉取 │ GitRepository (CRD) │
│ │ 「声明从哪拉代码」 │
│ │ url / ref.branch / interval │
│ │ → 由 source-controller 处理 │
│ └───────────────────────────────┘
│ │ ③ 下载,按 commit SHA 缓存成 artifact
│ ▼
│ ┌───────────────────────────────┐
│ │ 制品快照 stored artifact │
│ │ master/<sha> │
│ └───────────────────────────────┘
│ │ ④ 从快照读 YAML 清单
│ ▼
│ ┌───────────────────────────────┐
│ │ Kustomization (CRD) │
│ │ 「声明把清单 apply 到集群」 │
│ │ sourceRef/path/prune/interval │
│ │ → 由 kustomize-controller 处理│
│ └───────────────────────────────┘
│ │ ⑤ 与集群现状 diff,只改差异
│ ▼
│ ┌───────────────────────────────┐
│ │ Deployment hello-world-flask │
│ │ replicas: 2 │
│ └───────────────────────────────┘
│ │ ⑥ kubelet 调度
│ ▼
│ ┌───────────────────────────────┐
│ │ Pod ×2(拉镜像/启动) │
│ └───────────────────────────────┘
│
└── 一切以 Git 为准:手动改了集群,下个周期会改回来5. 踩坑记录(本章最有价值的部分)
坑 1:GitRepository 三连坑
- URL 写错:照抄教程作者的
lyzhang1999/fluxcd-demo,应该指向自己的仓库 - 分支写错:清单写
main,自己的仓库却是master - 认证模式选错:一开始按"私有仓库"思路配 SSH + secret,其实公共仓库直接 HTTPS 匿名拉取即可,连 secret 都不用
- 教训:写 GitRepository 先确认——仓库是谁的、公开还是私有、分支名是什么。公共仓库用 HTTPS 最省事。
坑 2:Pod 不走宿主机 /etc/hosts(最容易忽略)
- 现象:宿主机 /etc/hosts 修好了 GitHub,但 Flux source-controller 照样超时
- 本质:Pod 走 CoreDNS 解析,与宿主机 /etc/hosts 完全隔离
- 解决:给 Pod 修 DNS 要改 CoreDNS 的
NodeHosts(hosts 插件 reload 15s 自动生效),或重启相关 controller 清 DNS 缓存
坑 3:Flux SSH 方案的 "knownhosts: key mismatch"
- 现象:source-controller 报
ssh: handshake failed: knownhosts: key mismatch,明明宿主机 SSH 正常、known_hosts 内容正确 - 本质:SSH 主机密钥校验在容器里坑很多(解析路径、网络干扰都可能触发)
- 解决:公共仓库绕开它,直接 HTTPS;私有仓库再考虑 SSH
坑 4:Flux Kustomization 会"帮倒忙"覆盖你的手动修复
- 现象:手动
kubectl set image修好镜像后,Flux Kustomization 应用旧仓库的 deployment.yaml,把镜像改回拉不动的版本 → 新 ReplicaSet ImagePullBackOff,出现 3 个 Pod - 本质:只要 Kustomization 在跑,它就以仓库内容为准,手动改集群会被回滚
- 排查技巧:看到莫名多出的 Pod/ReplicaSet,先想是不是有控制器(Flux/Helm)在管理;调试期间用
spec.suspend: true挂起 Kustomization
结语
GitOps 的核心一句话:一切以 Git 为准。Git 仓库是唯一事实来源,集群状态持续向它收敛;发布可审计、可回滚、可复现。而手动改集群的"快捷操作",在 GitOps 里反而是最危险的操作——你改的,下个 5 秒周期就会被改回来。