AI掘金头条项目-K8s部署实战教程

FastAPI + Vue3 全栈项目 Kubernetes 部署实战教程

从零开始,将 FastAPI 后端 + Vue3 前端 + MySQL + Redis 全栈应用完整部署到 Kubernetes 集群。

涵盖镜像构建、containerd 运行时适配、存储卷配置、故障排错等生产实战内容。


目录

  • [1. 项目架构概览](#1. 项目架构概览)
  • [2. 环境准备](#2. 环境准备)
  • [3. 项目代码结构](#3. 项目代码结构)
  • [4. Kubernetes 资源配置说明](#4. Kubernetes 资源配置说明)
  • [5. 部署脚本](#5. 部署脚本)
  • [6. 完整部署流程](#6. 完整部署流程)
  • [7. 访问验证](#7. 访问验证)
  • [8. 故障排查手册](#8. 故障排查手册)
  • [9. 常用运维命令](#9. 常用运维命令)
  • [10. 总结](#10. 总结)

1. 项目架构概览

1.1 技术栈

层级 技术 说明
后端 FastAPI + SQLAlchemy (async) + Uvicorn Python 异步 Web 框架
前端 Vue 3 + Vite + Pinia + Vant UI 单页面应用
数据库 MySQL 8.0 持久化存储
缓存 Redis 7 热点数据缓存
反向代理 Nginx 前端静态资源 + API 代理
容器编排 Kubernetes v1.31 3 节点集群
容器运行时 containerd 2.2 无 Docker 环境
网络 Cilium CNI 容器网络

1.2 K8s 部署架构图

复制代码
┌──────────────────────────────────────────────────┐
│                   客户端浏览器                      │
│           http://192.168.194.21:30080             │
└──────────────────────┬───────────────────────────┘
                       │ NodePort 30080
┌──────────────────────▼───────────────────────────┐
│              Frontend Service (NodePort)          │
│              frontend.news-app.svc                │
│              Port: 80 → NodePort: 30080           │
└──────────────────────┬───────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────┐
│            Frontend Deployment (2 replicas)        │
│  ┌─────────────────────────────────────────────┐ │
│  │  Nginx (反向代理)                             │ │
│  │  ├── /          → Vue3 静态文件               │ │
│  │  └── /api/*     → backend:8000               │ │
│  └─────────────────────────────────────────────┘ │
└──────────────────────┬───────────────────────────┘
                       │ /api/* 代理
┌──────────────────────▼───────────────────────────┐
│              Backend Service (ClusterIP)           │
│              backend.news-app.svc:8000            │
└──────────────────────┬───────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────┐
│            Backend Deployment (2 replicas)         │
│  ┌─────────────────────────────────────────────┐ │
│  │  FastAPI + Uvicorn (:8000)                   │ │
│  │  ├── /api/news/*     新闻接口                │ │
│  │  ├── /api/user/*     用户接口                │ │
│  │  └── /api/favorite/* 收藏接口                │ │
│  └──────────────────────┬──────────┬───────────┘ │
└─────────────────────────┼──────────┼─────────────┘
                          │          │
              ┌───────────▼──┐  ┌───▼───────────┐
              │ MySQL (1副本) │  │ Redis (1副本)  │
              │ :3306         │  │ :6379          │
              │ hostPath PV   │  │ ClusterIP      │
              │ 固定在 master │  │                │
              └───────────────┘  └────────────────┘

1.3 K8s 资源清单

资源类型 名称 用途
Namespace news-app 隔离所有资源
Secret news-secret MySQL 密码等敏感配置
ConfigMap nginx-config Nginx 反向代理配置
ConfigMap mysql-init-sql 数据库初始化 SQL
PersistentVolume mysql-pv MySQL 数据持久化 (hostPath)
PersistentVolumeClaim mysql-pvc PV 绑定请求
Deployment mysql MySQL 8.0 单副本
Service mysql MySQL ClusterIP
Deployment redis Redis 7 单副本
Service redis Redis ClusterIP
Deployment backend FastAPI 后端 2 副本
Service backend 后端 ClusterIP
Deployment frontend Nginx+Vue 前端 2 副本
Service frontend 前端 NodePort

2. 环境准备

2.1 集群环境

本文基于以下真实环境完成部署:

项目 配置
操作系统 Red Hat Enterprise Linux 10.1
内核版本 6.12.0
Kubernetes v1.31.14
容器运行时 containerd 2.2.3
网络插件 Cilium
节点数 3 台

节点信息:

主机名 角色 IP 地址
master control-plane 192.168.194.21
node1 worker 192.168.194.22
node2 worker 192.168.194.23

2.2 前置检查

在 master 节点上执行以下命令,确认环境就绪:

bash 复制代码
# 1. 检查集群版本
kubectl version --short

# 2. 检查节点状态(所有节点必须 Ready)
kubectl get nodes -o wide

# 3. 确认容器运行时
crictl ps 2>/dev/null && echo "使用 containerd/CRI-O"

# 4. 检查是否有 StorageClass(本文方案有无均可)
kubectl get storageclass

# 5. 检查是否有 Ingress Controller(本文方案有无均可)
kubectl get pods -A | grep -i ingress

2.3 安装 Docker(用于构建镜像)

注意:集群使用 containerd 运行容器,但我们仍需要 Docker 来构建镜像。

bash 复制代码
# RHEL/CentOS 安装 Docker
yum install -y docker
systemctl start docker
systemctl enable docker

# 验证
docker --version

3. 项目代码结构

3.1 目录结构

复制代码
fastapi/
├── database.sql                     # MySQL 建表 + 种子数据
├── docker-compose.yml               # 原 Docker Compose 部署文件
│
├── toutiao_backend/                 # FastAPI 后端
│   ├── Dockerfile                   # 后端镜像构建文件
│   ├── .dockerignore                # ⚠️ 注意:需要修复!
│   ├── requirements.txt             # Python 依赖
│   ├── main.py                      # 应用入口
│   ├── config/
│   │   ├── db_config.py             # MySQL 连接配置(读环境变量)
│   │   └── cache_conf.py            # Redis 连接配置(读环境变量)
│   ├── models/                      # SQLAlchemy ORM 模型
│   ├── schemas/                     # Pydantic 请求/响应模型
│   ├── routers/                     # API 路由
│   ├── crud/                        # 数据库操作
│   ├── cache/                       # ⚠️ Redis 缓存模块
│   └── utils/                       # 工具函数
│
├── xwzx-news/                       # Vue 3 前端
│   ├── Dockerfile                   # 前端镜像构建文件(多阶段)
│   ├── nginx.conf                   # Nginx 反向代理配置
│   └── src/                         # Vue 源码
│
└── k8s/                             # Kubernetes 部署配置
    ├── namespace.yaml               # 命名空间
    ├── secret.yaml                  # 密钥
    ├── configmap.yaml               # Nginx 配置
    ├── mysql.yaml                   # MySQL PV+PVC+Deployment+Service
    ├── redis.yaml                   # Redis Deployment+Service
    ├── backend.yaml                 # FastAPI Deployment+Service
    ├── frontend.yaml                # Nginx+Vue Deployment+Service
    ├── deploy.sh                    # 一键部署脚本
    └── build-and-import.sh          # 镜像构建与分发脚本

3.2 关键配置文件

3.2.1 后端 Dockerfile

文件路径:toutiao_backend/Dockerfile

dockerfile 复制代码
FROM python:3.12-slim

WORKDIR /app

# 安装 gcc(bcrypt 需要编译)
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

# 复制应用代码
COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
3.2.2 ⚠️ .dockerignore 修复(重要!)

文件路径:toutiao_backend/.dockerignore

错误内容(修复前):

dockerignore 复制代码
__pycache__
*.pyc
.venv
.env
.git
.gitignore
*.md
test_*
.idea
cache          # ❌ 这行会把 cache/ 源代码目录也排除掉!

正确内容(修复后):

dockerignore 复制代码
__pycache__
*.pyc
.venv
.env
.git
.gitignore
*.md
test_*
.idea

错误说明: 第 10 行的 cache 原本想排除 __pycache__ 目录,但它同时匹配了项目中的 cache/ 源码目录(包含 news_cache.py 等 Redis 缓存模块)。这会导致构建出的镜像缺少 cache 模块,后端启动时抛出 ModuleNotFoundError: No module named 'cache'

3.2.3 前端 Dockerfile(多阶段构建)

文件路径:xwzx-news/Dockerfile

dockerfile 复制代码
# 阶段一:编译 Vue 应用
FROM node:22-alpine AS builder

WORKDIR /app

COPY package.json ./
RUN npm install

COPY . .

ARG VITE_API_BASE_URL=
RUN VITE_API_BASE_URL=${VITE_API_BASE_URL} npm run build

# 阶段二:Nginx 运行时
FROM nginx:alpine

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 复制 Nginx 配置(运行时会通过 ConfigMap 覆盖)
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
3.2.4 前端 Nginx 配置

文件路径:xwzx-news/nginx.conf(此内容会通过 K8s ConfigMap 挂载覆盖)

nginx 复制代码
server {
    listen       80;
    server_name  localhost;

    # 前端静态文件(Vue History 模式)
    location / {
        root   /usr/share/nginx/html;
        index  index.html;
        try_files $uri $uri/ /index.html;
    }

    # API 反向代理到后端 Service
    location /api/ {
        proxy_pass http://backend:8000;          # ← K8s Service 名称
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
3.2.5 数据库配置(读环境变量)

文件路径:toutiao_backend/config/db_config.py

python 复制代码
import os
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine

DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "3306")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "123456")
DB_NAME = os.getenv("DB_NAME", "news_app")

ASYNC_DATABASE_URL = f"mysql+aiomysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"

async_engine = create_async_engine(
    ASYNC_DATABASE_URL,
    echo=True,
    pool_size=10,
    max_overflow=20
)

AsyncSessionLocal = async_sessionmaker(
    bind=async_engine,
    class_=AsyncSession,
    expire_on_commit=False
)

async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()
3.2.6 Redis 配置(读环境变量)

文件路径:toutiao_backend/config/cache_conf.py

python 复制代码
import os
import redis.asyncio as redis

REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
REDIS_DB = int(os.getenv("REDIS_DB", "0"))

redis_client = redis.Redis(
    host=REDIS_HOST,
    port=REDIS_PORT,
    db=REDIS_DB,
    decode_responses=True
)

4. Kubernetes 资源配置说明

4.1 命名空间(namespace.yaml)

yaml 复制代码
apiVersion: v1
kind: Namespace
metadata:
  name: news-app

将所有资源放在 news-app 命名空间下,实现逻辑隔离。后续所有资源都通过 namespace: news-app 归属到此命名空间。


4.2 密钥(secret.yaml)

yaml 复制代码
apiVersion: v1
kind: Secret
metadata:
  name: news-secret
  namespace: news-app
type: Opaque
stringData:
  mysql-root-password: "123456"
  mysql-user: "root"
  mysql-password: "123456"
  mysql-database: "news_app"

说明: 使用 stringData 字段可以直接写明文,K8s 会自动进行 Base64 编码存储。生产环境建议使用 kubectl create secret 命令行创建或使用 Sealed Secrets。


4.3 Nginx 配置 ConfigMap(configmap.yaml)

yaml 复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-config
  namespace: news-app
data:
  default.conf: |
    server {
        listen       80;
        server_name  localhost;

        location / {
            root   /usr/share/nginx/html;
            index  index.html;
            try_files $uri $uri/ /index.html;
        }

        location /api/ {
            proxy_pass http://backend:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }

关键设计: proxy_pass http://backend:8000 中的 backend 是 K8s Service 的名称。由于前端和后端在同一个 namespace(news-app),K8s 内部 DNS 会自动将 backend 解析为后端的 ClusterIP。


4.4 MySQL(mysql.yaml)

MySQL 是最复杂的资源,包含 4 个 K8s 对象:

4.4.1 PersistentVolume(主机路径卷)
yaml 复制代码
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv
  namespace: news-app
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /data/mysql

说明: 由于集群没有 StorageClass,无法自动创建 PV。我们手动创建一个 hostPath 类型的 PV,数据存储在节点的 /data/mysql 目录。

注意: hostPath 适合开发/测试环境。生产环境应使用 NAS、Ceph RBD 或云厂商的 StorageClass。

4.4.2 PersistentVolumeClaim(存储声明)
yaml 复制代码
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
  namespace: news-app
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
4.4.3 Deployment + Service
yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: news-app
  labels:
    app: mysql
spec:
  replicas: 1                          # 数据库单副本
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      nodeSelector:                     # 固定在 master 节点
        kubernetes.io/hostname: master
      tolerations:                      # 容忍 master 污点
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule
      containers:
        - name: mysql
          image: mysql:8.0
          ports:
            - containerPort: 3306
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: news-secret
                  key: mysql-root-password
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: news-secret
                  key: mysql-database
          args:
            - --character-set-server=utf8mb4
            - --collation-server=utf8mb4_unicode_ci
            - --init-connect=SET NAMES utf8mb4
            - --skip-character-set-client-handshake
          volumeMounts:
            - name: mysql-data
              mountPath: /var/lib/mysql           # 数据目录
            - name: mysql-init
              mountPath: /docker-entrypoint-initdb.d  # 初始化脚本目录
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:                          # 存活探针
            exec:
              command:
                - mysqladmin
                - ping
                - -h
                - localhost
                - -u
                - root
                - -p123456
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:                         # 就绪探针
            exec:
              command:
                - mysqladmin
                - ping
                - -h
                - localhost
                - -u
                - root
                - -p123456
            initialDelaySeconds: 10
            periodSeconds: 5
      volumes:
        - name: mysql-data
          persistentVolumeClaim:
            claimName: mysql-pvc
        - name: mysql-init
          configMap:
            name: mysql-init-sql
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: news-app
spec:
  selector:
    app: mysql
  ports:
    - port: 3306
      targetPort: 3306
  type: ClusterIP

关键设计决策说明:

设计点 决策 原因
nodeSelector 固定到 master hostPath PV 在 master 上,Pod 必须调度到同一节点
tolerations 容忍 control-plane 污点 master 节点默认有 NoSchedule 污点,不加 toleration Pod 无法调度
初始化方式 ConfigMap 挂载 SQL MySQL 镜像会自动执行 /docker-entrypoint-initdb.d/ 下的 .sql 文件
初始化 ConfigMap mysql-init-sql deploy.sh 脚本从 database.sql 文件动态创建

4.5 Redis(redis.yaml)

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: news-app
  labels:
    app: redis
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:7-alpine
          ports:
            - containerPort: 6379
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
              cpu: "200m"
          livenessProbe:
            exec:
              command:
                - redis-cli
                - ping
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - redis-cli
                - ping
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: news-app
spec:
  selector:
    app: redis
  ports:
    - port: 6379
      targetPort: 6379
  type: ClusterIP

说明: Redis 作为缓存服务,不持久化数据,使用最简单的 Deployment + ClusterIP 部署方式。


4.6 后端(backend.yaml)

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: news-app
  labels:
    app: backend
spec:
  replicas: 2                          # 2 副本高可用
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
        - name: backend
          image: news-backend:latest    # 自定义镜像
          imagePullPolicy: IfNotPresent # 不从仓库拉取
          ports:
            - containerPort: 8000
          env:
            - name: DB_HOST
              value: "mysql"            # ← K8s Service 名称
            - name: DB_PORT
              value: "3306"
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: news-secret
                  key: mysql-user
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: news-secret
                  key: mysql-password
            - name: DB_NAME
              valueFrom:
                secretKeyRef:
                  name: news-secret
                  key: mysql-database
            - name: REDIS_HOST
              value: "redis"            # ← K8s Service 名称
            - name: REDIS_PORT
              value: "6379"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "300m"
          livenessProbe:
            httpGet:
              path: /
              port: 8000
            initialDelaySeconds: 15
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: news-app
spec:
  selector:
    app: backend
  ports:
    - port: 8000
      targetPort: 8000
  type: ClusterIP                    # 仅集群内部访问

关键配置说明:

配置项 说明
imagePullPolicy IfNotPresent 镜像已通过 ctr import 导入到 containerd,不从在线仓库拉取
DB_HOST mysql K8s 内部 DNS,解析为 MySQL Service 的 ClusterIP
REDIS_HOST redis K8s 内部 DNS,解析为 Redis Service 的 ClusterIP
replicas 2 两个副本提供基本的高可用和负载分担

4.7 前端(frontend.yaml)

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: news-app
  labels:
    app: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - name: frontend
          image: news-frontend:latest
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/conf.d/default.conf
              subPath: default.conf       # 只覆盖这一个文件
          resources:
            requests:
              memory: "64Mi"
              cpu: "50m"
            limits:
              memory: "128Mi"
              cpu: "200m"
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: news-app
spec:
  selector:
    app: frontend
  type: NodePort                     # 对外暴露访问
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080                # 固定 NodePort 端口

关键配置说明:

配置项 说明
type NodePort 没有 Ingress Controller 时,通过节点 IP 直接访问
nodePort 30080 固定端口,访问 http://<任意节点IP>:30080
subPath default.conf 只覆盖 nginx 配置文件,不影响容器内其他文件

5. 部署脚本

5.1 一键部署脚本(deploy.sh

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
NAMESPACE="news-app"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }
warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }

# ---------- step 1: 检查前置条件 ----------
info "检查前置条件..."
command -v kubectl >/dev/null 2>&1 || { error "请先安装 kubectl"; exit 1; }

# ---------- step 2: 创建 namespace ----------
info "创建 Namespace..."
kubectl apply -f "${SCRIPT_DIR}/namespace.yaml"

# ---------- step 3: 从 database.sql 创建 ConfigMap ----------
info "创建 MySQL 初始化脚本 ConfigMap..."
kubectl create configmap mysql-init-sql \
    --from-file=init.sql="${PROJECT_DIR}/database.sql" \
    --namespace="${NAMESPACE}" \
    --dry-run=client -o yaml | kubectl apply -f -

# ---------- step 4: 部署所有资源 ----------
info "部署 Kubernetes 资源..."
kubectl apply -f "${SCRIPT_DIR}/secret.yaml"
kubectl apply -f "${SCRIPT_DIR}/configmap.yaml"
kubectl apply -f "${SCRIPT_DIR}/mysql.yaml"
kubectl apply -f "${SCRIPT_DIR}/redis.yaml"
kubectl apply -f "${SCRIPT_DIR}/backend.yaml"
kubectl apply -f "${SCRIPT_DIR}/frontend.yaml"

# ---------- step 5: 等待所有 Pod 就绪 ----------
info "等待 MySQL 就绪 (最长 120s)..."
kubectl wait --for=condition=ready pod -l app=mysql -n "${NAMESPACE}" --timeout=120s

info "等待 Redis 就绪 (最长 60s)..."
kubectl wait --for=condition=ready pod -l app=redis -n "${NAMESPACE}" --timeout=60s

info "等待 Backend 就绪 (最长 90s)..."
kubectl wait --for=condition=ready pod -l app=backend -n "${NAMESPACE}" --timeout=90s

info "等待 Frontend 就绪 (最长 60s)..."
kubectl wait --for=condition=ready pod -l app=frontend -n "${NAMESPACE}" --timeout=60s

# ---------- step 6: 输出访问信息 ----------
echo ""
info "============================================"
info "  部署完成!"
info "============================================"
echo ""
info "Pod 状态:"
kubectl get pods -n "${NAMESPACE}" -o wide
echo ""
info "Service 列表:"
kubectl get svc -n "${NAMESPACE}"
echo ""
info "访问地址 (NodePort):"
echo "  http://192.168.194.21:30080   (master)"
echo "  http://192.168.194.22:30080   (node1)"
echo "  http://192.168.194.23:30080   (node2)"

5.2 镜像构建与分发脚本(build-and-import.sh

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
IMAGE_BACKEND="news-backend:latest"
IMAGE_FRONTEND="news-frontend:latest"

# K8s 所有节点
NODES=("192.168.194.21" "192.168.194.22" "192.168.194.23")
# 获取当前节点 IP(避免 SSH 到自己)
LOCAL_IP=$(hostname -I | awk '{print $1}')

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC}  $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }

# ---------- 检测构建工具 ----------
BUILD_CMD=""
if command -v docker &>/dev/null; then
    BUILD_CMD="docker"
elif command -v podman &>/dev/null; then
    BUILD_CMD="podman"
elif command -v nerdctl &>/dev/null; then
    BUILD_CMD="nerdctl"
else
    error "未找到 docker / podman / nerdctl,请先安装其中之一"
    exit 1
fi
info "使用构建工具: ${BUILD_CMD}"

# ---------- 构建后端镜像 ----------
info "构建后端镜像..."
cd "${PROJECT_DIR}/toutiao_backend"
${BUILD_CMD} build -t "${IMAGE_BACKEND}" .

# ---------- 构建前端镜像 ----------
info "构建前端镜像..."
cd "${PROJECT_DIR}/xwzx-news"
${BUILD_CMD} build --build-arg VITE_API_BASE_URL="" -t "${IMAGE_FRONTEND}" .

# ---------- 导出镜像 ----------
info "导出镜像..."
${BUILD_CMD} save -o /tmp/news-backend.tar "${IMAGE_BACKEND}"
${BUILD_CMD} save -o /tmp/news-frontend.tar "${IMAGE_FRONTEND}"

# ---------- 分发到所有节点 ----------
# 先处理本地节点(不需要 scp 到自己)
info "在本地导入镜像到 containerd..."
ctr -n k8s.io images import /tmp/news-backend.tar
ctr -n k8s.io images import /tmp/news-frontend.tar

# 再处理远程节点
for node in "${NODES[@]}"; do
    if [ "${node}" = "${LOCAL_IP}" ]; then
        continue  # 本地已处理,跳过
    fi
    info "分发镜像到 ${node}..."
    scp /tmp/news-backend.tar /tmp/news-frontend.tar root@"${node}":/tmp/
    info "在 ${node} 上导入镜像到 containerd..."
    ssh root@"${node}" "ctr -n k8s.io images import /tmp/news-backend.tar && ctr -n k8s.io images import /tmp/news-frontend.tar && rm -f /tmp/news-backend.tar /tmp/news-frontend.tar"
done

# ---------- 清理本地 ----------
rm -f /tmp/news-backend.tar /tmp/news-frontend.tar

info "验证各节点镜像:"
for node in "${NODES[@]}"; do
    echo -n "  ${node}: "
    if [ "${node}" = "${LOCAL_IP}" ]; then
        crictl images | grep news- || echo "(无)"
    else
        ssh root@"${node}" "crictl images | grep news-" || echo "(无)"
    fi
done
echo ""
info "下一步: 运行 ./deploy.sh 部署应用"

脚本设计要点:

  1. 本地节点先在循环外单独处理(不 scp、不删文件),远程节点在循环中 scp → SSH 导入 → 远程清理
  2. 本地 tar 文件在循环全部结束后才删除,避免远程节点 scp 时源文件已被清理
  3. 支持 docker / podman / nerdctl 三种构建工具
  4. 构建完成后验证各节点是否成功导入

6. 完整部署流程

第一步:将项目代码拷贝到 master 节点

如果你在开发机(Windows/Mac)上,先通过 SCP 将项目拷贝到 master 节点:

bash 复制代码
# Windows 开发机上执行(PowerShell 或 WSL)
scp -r D:/claude-code-demo/fastapi root@192.168.194.21:/root/news-app/

如果代码已经在 master 节点上,跳过此步。

第二步:修复 .dockerignore(必须!)

bash 复制代码
cd /root/news-app/toutiao_backend

# 确认 .dockerignore 中没有裸的 "cache" 行
cat .dockerignore

# 正确内容应为(只排除 __pycache__,不排除 cache/ 源码目录):
# __pycache__
# *.pyc
# .venv
# .env
# .git
# .gitignore
# *.md
# test_*
# .idea

如果看到单独一行的 cache,用编辑器删除该行。

第三步:创建 MySQL 数据目录

bash 复制代码
mkdir -p /data/mysql

此目录对应 mysql.yaml 中 hostPath PV 的路径。

第四步:构建镜像并分发到所有节点

bash 复制代码
cd /root/news-app/k8s
chmod +x build-and-import.sh
./build-and-import.sh

这一步会:

  1. 用 Docker 构建 news-backend:latest 镜像(约 135MB)
  2. 用 Docker 构建 news-frontend:latest 镜像(约 25MB)
  3. 导出为 tar 文件
  4. 复制到 node1、node2
  5. 通过 ctr -n k8s.io images import 导入到所有节点的 containerd

第五步:部署到 Kubernetes

bash 复制代码
cd /root/news-app/k8s
chmod +x deploy.sh
./deploy.sh

如果 deploy.sh 执行出错(namespace 未创建导致),可以手动按顺序执行:

bash 复制代码
# 1. 创建 Namespace
kubectl apply -f namespace.yaml

# 2. 创建 MySQL 初始化 ConfigMap
kubectl create configmap mysql-init-sql \
    --from-file=init.sql=../database.sql \
    --namespace=news-app \
    --dry-run=client -o yaml | kubectl apply -f -

# 3. 部署所有资源
kubectl apply -f secret.yaml
kubectl apply -f configmap.yaml
kubectl apply -f mysql.yaml
kubectl apply -f redis.yaml
kubectl apply -f backend.yaml
kubectl apply -f frontend.yaml

第六步:等待 Pod 就绪

bash 复制代码
# 实时查看 Pod 启动状态
kubectl get pods -n news-app -o wide -w

Ctrl+C 退出 watch。所有 Pod 的 READY 列显示 1/1 且 STATUS 为 Running 即表示成功。

正常输出示例:

复制代码
NAME                        READY   STATUS    RESTARTS   AGE   IP            NODE
backend-596df5cf6f-xxxxx    1/1     Running   0          2m    10.0.2.85     node1
backend-596df5cf6f-yyyyy    1/1     Running   0          2m    10.0.0.158    node2
frontend-65cbb4b469-xxxxx   1/1     Running   0          2m    10.0.2.156    node1
frontend-65cbb4b469-yyyyy   1/1     Running   0          2m    10.0.0.91     node2
mysql-8484d459d8-xxxxx      1/1     Running   0          3m    10.0.1.50     master
redis-c59784688-xxxxx       1/1     Running   0          2m    10.0.2.52     node1

7. 访问验证

7.1 浏览器访问

部署成功后,通过任意节点的 IP + NodePort 访问:

复制代码
http://192.168.194.21:30080
http://192.168.194.22:30080
http://192.168.194.23:30080

7.2 API 功能验证

bash 复制代码
# 测试后端 API
kubectl port-forward -n news-app svc/backend 8000:8000 &

# 测试接口
curl http://localhost:8000/
# 返回: {"message":"Hello World"}

curl http://localhost:8000/api/news/categories
# 返回: 新闻分类列表

curl -X POST http://localhost:8000/api/user/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'
# 返回: 登录成功 + token

7.3 验证前端 Nginx 代理

bash 复制代码
# 通过 NodePort 测试 API 代理
curl http://192.168.194.21:30080/api/news/categories
# 应返回与直接调用后端相同的结果

8. 故障排查手册

以下是部署过程中可能遇到的常见错误及解决方案。

错误 1:Backend CrashLoopBackOff --- ModuleNotFoundError: No module named 'cache'

错误日志:

复制代码
ModuleNotFoundError: No module named 'cache'
File "/app/crud/news.py", line 5, in <module>
    from cache.news_cache import get_cached_categories

原因: .dockerignore 文件中有一行单独的 cache,Docker 构建时把 cache/ 源码目录排除了。

解决:

bash 复制代码
# 编辑 toutiao_backend/.dockerignore,删除裸的 "cache" 行
# 修复后应该是:
#   __pycache__
#   *.pyc
#   ...
#   .idea
# (没有单独的 "cache")

# 重新构建并分发镜像
cd /root/news-app/toutiao_backend
docker build -t news-backend:latest .

# 重新导出并分发
docker save -o /tmp/news-backend.tar news-backend:latest

# 导入到本地
ctr -n k8s.io images import /tmp/news-backend.tar

# 分发到其他节点
scp /tmp/news-backend.tar root@192.168.194.22:/tmp/
ssh root@192.168.194.22 "ctr -n k8s.io images import /tmp/news-backend.tar"
scp /tmp/news-backend.tar root@192.168.194.23:/tmp/
ssh root@192.168.194.23 "ctr -n k8s.io images import /tmp/news-backend.tar"

# 重建 Pod
kubectl delete pods -l app=backend -n news-app

错误 2:MySQL Pending --- pod has unbound immediate PersistentVolumeClaims

错误信息:

复制代码
Warning  FailedScheduling  0/3 nodes are available: pod has unbound immediate PersistentVolumeClaims.

排查步骤:

bash 复制代码
# 检查 PVC 状态
kubectl get pvc -n news-app
# STATUS: Pending → 说明 PV 未创建或无 StorageClass

# 检查 StorageClass
kubectl get storageclass
# No resources found → 确认没有 StorageClass

原因: 集群没有部署 StorageClass,PVC 无法动态创建 PV。

解决:

bash 复制代码
# 方法一:使用 hostPath PV(本文方案)
mkdir -p /data/mysql
kubectl apply -f mysql.yaml   # 已包含 PV 定义

# 方法二:安装 local-path-provisioner
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl annotate storageclass local-path storageclass.kubernetes.io/is-default-class=true

错误 3:MySQL Pending --- node(s) had untolerated taint

错误信息:

复制代码
Warning  FailedScheduling  0/3 nodes are available:
  1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
  2 node(s) didn't match Pod's node affinity/selector.

原因: MySQL 配置了 nodeSelector: kubernetes.io/hostname: master,但 master 节点有 control-plane 污点(NoSchedule),Pod 无法调度。

排查污点:

bash 复制代码
kubectl describe node master | grep -i taint
# 输出: Taints: node-role.kubernetes.io/control-plane:NoSchedule

解决: 在 Deployment 的 spec.template.spec 下添加 tolerations:

yaml 复制代码
tolerations:
  - key: node-role.kubernetes.io/control-plane
    operator: Exists
    effect: NoSchedule

已在 mysql.yaml 中修复。

错误 4:build-and-import.sh 分发镜像报错 --- No such file or directory

错误信息:

复制代码
scp: stat local "/tmp/news-backend.tar": No such file or directory

原因: 旧版脚本在循环中对所有节点先 scp 再导入,本地节点导入后立即 rm -f 删掉了 tar 文件,导致后续远程节点 scp 时源文件已不存在。

解决: 将本地节点的导入提到循环之前执行,远程节点在循环中 scp → SSH 导入 → 远程清理。本地 tar 文件等所有节点处理完毕后再删除。

错误 5:configmap 创建失败 --- namespaces not found

错误信息:

复制代码
Error from server (NotFound): error when creating "STDIN": namespaces "news-app" not found

原因: 部署脚本中 kubectl create configmapkubectl apply -f namespace.yaml 之前执行。

解决: 确保 namespace 先创建:

bash 复制代码
kubectl apply -f namespace.yaml
# 然后再创建 ConfigMap
kubectl create configmap mysql-init-sql \
    --from-file=init.sql=../database.sql \
    --namespace=news-app \
    --dry-run=client -o yaml | kubectl apply -f -

已在新版 deploy.sh 中调整顺序。

错误 6:镜像拉取失败 --- ImagePullBackOff

错误信息:

复制代码
Failed to pull image "news-backend:latest": ... pull access denied

原因: imagePullPolicy 未设置或设置为 Always,K8s 尝试从 Docker Hub 拉取自定义镜像。

解决: 在 Deployment 中设置:

yaml 复制代码
imagePullPolicy: IfNotPresent

已在所有自定义镜像的 Deployment 中配置。


9. 常用运维命令

9.1 查看状态

bash 复制代码
# 查看所有 Pod
kubectl get pods -n news-app -o wide

# 查看所有 Service
kubectl get svc -n news-app

# 查看所有资源
kubectl get all -n news-app

# 查看 PV/PVC
kubectl get pv,pvc -n news-app

# 实时监控 Pod 变化
kubectl get pods -n news-app -w

9.2 查看日志

bash 复制代码
# 查看后端日志
kubectl logs -l app=backend -n news-app --tail=50

# 实时查看后端日志
kubectl logs -l app=backend -n news-app -f

# 查看前端日志
kubectl logs -l app=frontend -n news-app

# 查看 MySQL 日志
kubectl logs -l app=mysql -n news-app

# 查看上一个崩溃容器的日志
kubectl logs -l app=backend -n news-app --previous

9.3 进入容器调试

bash 复制代码
# 进入后端容器
kubectl exec -it -n news-app $(kubectl get pod -n news-app -l app=backend -o jsonpath='{.items[0].metadata.name}') -- /bin/bash

# 进入 MySQL 容器
kubectl exec -it -n news-app $(kubectl get pod -n news-app -l app=mysql -o jsonpath='{.items[0].metadata.name}') -- mysql -u root -p123456

9.4 扩缩容

bash 复制代码
# 后端扩容到 3 个副本
kubectl scale deployment backend -n news-app --replicas=3

# 缩容到 1 个
kubectl scale deployment backend -n news-app --replicas=1

9.5 滚动重启

bash 复制代码
# 重启后端(滚动更新模式)
kubectl rollout restart deployment backend -n news-app

# 重启前端
kubectl rollout restart deployment frontend -n news-app

9.6 端口转发(本地调试)

bash 复制代码
# 转发后端到本机
kubectl port-forward -n news-app svc/backend 8000:8000

# 转发前端到本机
kubectl port-forward -n news-app svc/frontend 8080:80

# 转发 MySQL 到本机
kubectl port-forward -n news-app svc/mysql 3306:3306

9.7 卸载应用

bash 复制代码
# 删除整个 namespace(级联删除所有资源)
kubectl delete namespace news-app

# 或逐个删除资源文件
kubectl delete -f k8s/

10. 总结

10.1 部署清单速查

序号 步骤 命令
1 拷贝代码到 master scp -r fastapi root@master:/root/news-app/
2 修复 .dockerignore 删除 cache
3 创建数据目录 mkdir -p /data/mysql
4 构建并分发镜像 cd k8s && ./build-and-import.sh
5 部署应用 ./deploy.sh
6 检查 Pod kubectl get pods -n news-app
7 访问 http://192.168.194.21:30080

10.2 核心知识点总结

  1. containerd 与 Docker 的关系

    • K8s 1.24+ 默认使用 containerd 运行容器
    • 构建镜像仍需要 Docker / podman / nerdctl
    • 导入镜像到 containerd:ctr -n k8s.io images import <file>.tar
    • 查看 containerd 镜像:crictl images
  2. K8s 内部服务发现

    • 同 namespace 下 Service 名称即为 DNS:mysqlredisbackend
    • 应用代码中不需要 IP,直接用 Service 名称连接
  3. 存储方案选型

    • 有 StorageClass:直接创建 PVC,自动分配 PV
    • 无 StorageClass:手动创建 hostPath PV(开发/测试)
    • hostPath PV 需配合 nodeSelector 将 Pod 固定在指定节点
  4. 污点与容忍

    • master 节点默认有 NoSchedule 污点
    • 需要将 Pod 调度到 master 时,必须添加对应的 tolerations
  5. 镜像拉取策略

    • IfNotPresent:本地有就用本地(适合手动导入的场景)
    • Always:每次都从仓库拉取(适合有私有镜像仓库的场景)
    • Never:只使用本地镜像,没有就报错
  6. ConfigMap 的使用场景

    • Nginx 配置(运行时覆盖容器内配置)
    • MySQL 初始化脚本(SQL 文件挂载到 initdb 目录)
    • 使用 subPath 挂载单个文件而不覆盖整个目录

10.3 生产环境改进建议

当前方案 生产建议 原因
hostPath PV CSI 驱动 (NAS/Ceph/云盘) 数据高可用、节点故障自动迁移
NodePort Ingress Controller + 域名 SSL 终止、负载均衡、灰度发布
Secret 明文 Sealed Secrets / Vault 敏感信息加密、GitOps 友好
单副本 MySQL StatefulSet + 主从 数据库高可用
手动导入镜像 私有镜像仓库 (Harbor) 镜像版本管理、安全扫描
资源限制偏低 根据压测结果调整 避免 OOM Kill

全文完。 按照本文的步骤操作,你应该能够在自己的 Kubernetes 集群上成功部署这个 FastAPI + Vue3 全栈应用。如有问题,可以查阅第 8 节故障排查手册。

相关推荐
AI攻城狮2 小时前
DeepSeek V4:LLM 世界的"好又多"超市
云原生
观北海2 小时前
从 Sim2Sim 到 Sim2Real:以 ONNX 为核心的机器人策略实机落地全指南
python·机器人
AI精钢2 小时前
AI Agent 从上线到删库跑路始末
网络·人工智能·云原生·aigc
MATLAB代码顾问3 小时前
Python实现蜂群算法优化TSP问题
开发语言·python·算法
yaodong5183 小时前
不会Python也能数据分析:Gemini 3.1 Pro解决办公问题的SQL自动生成
python·sql·数据分析
BU摆烂会噶3 小时前
【LangGraph】持久化实现的三大能力——时间旅行
数据库·人工智能·python·postgresql·langchain
AI攻城狮3 小时前
RAG 的 Chunking 有什么好方案?从原理到实战选型
云原生
消失的旧时光-19434 小时前
统一并发模型:线程、Reactor、协程本质是一件事(从线程到协程 · 第6篇·终章)
java·python·算法
zhaoyong2226 小时前
MySQL 存储过程中字符集与排序规则不匹配导致查询性能下降的解决方案
jvm·数据库·python