Skip to content

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> bash

1.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.3

Dockerfilepython:3.8-slim-bookworm 基础镜像;装上 procps(提供 killall,后面自愈测试要用)、vimapache2-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:latest

1.3 发布到镜像仓库

bash
docker login

docker tag hello-world-flask robotzsj/hello-world-flask

docker push robotzsj/hello-world-flask

2. 部署到 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.0

2.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 -oyaml

Service(ClusterIP,集群内访问入口):

bash
kubectl create service clusterip hello-world-flask --tcp=5000:5000

Ingress(对外访问):注意 k3s 自带 traefik,所以 ingressClassNametraefik 而不是 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=LoadBalancer

2.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-server

2. 创建扩缩容策略(CPU 超 50% 自动扩容,副本 2~10):

bash
kubectl autoscale deployment hello-world-flask --cpu-percent=50 --min=2 --max=10

3. 设置资源配额:⚠️ 不给 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  Running

4. GitOps 工作流(Flux CD)

4.1 传统发布方式的三种更新

  1. kubectl set image 直接改运行中的应用
  2. 修改本地 manifest 再 kubectl apply
  3. kubectl edit 改集群内的 manifest

共同缺点:手动、不可审计、集群状态容易漂移。GitOps 把这一切换成「改 Git → 自动同步」。

4.2 安装 Flux CD

bash
kubectl apply -f https://raw.githubusercontent.com/lyzhang1999/resource/main/fluxcd/fluxcd.yaml

4.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-flask
bash
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.git
bash
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: default
bash
kubectl apply -f fluxcd-kustomize.yaml
kubectl get kustomization

Flux 每 5s 对比一次工作负载差异:识别到期望状态和集群实际状态存在差异时,触发重新部署

4.4 自动发布

修改仓库里的 deployment.yaml 镜像 tag → 推送:

bash
git add -A && git commit -m "Update image tag to v1"
git push origin master

Flux 自动应用;用下面命令查看触发重新部署的事件:

bash
kubectl describe kustomization hello-world-flask

4.5 快速回滚

Git 即回滚:

bash
git reset --hard <commit>
git push origin main -f

4.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 秒周期就会被改回来。

Meoo By Meoo 秒悟