Files
cloudnest/CloudNest-部署指南.md
T

32 KiB
Raw Blame History

CloudNest(云巢)— 轻量级个人云存储平台部署指南

项目定位:用 Golang 构建的个人云存储平台,学习 K8s 编排、微服务架构、分布式存储的实战项目。 服务器要求2核 CPU / 2GB 内存 / 40GB 硬盘 / 有域名 技术栈Go + Gin + MySQL + Redis + MinIO + k3s + Docker


一、项目架构总览

                    ┌─────────────────────────────────┐
                    │         域名 (yourdomain.com)     │
                    └──────────────┬──────────────────┘
                                   │
                    ┌──────────────▼──────────────────┐
                    │   Traefik Ingress (k3s自带)      │
                    │   - SSL自动证书(Let's Encrypt)    │
                    │   - 路由分发                      │
                    └──┬───────────────────┬──────────┘
                       │                   │
            ┌──────────▼─────┐   ┌────────▼──────────┐
            │  Auth Service   │   │   File Service    │
            │  (认证服务)      │   │   (文件服务)       │
            │  端口: 8081     │   │   端口: 8082      │
            └──────┬──────────┘   └──┬────────┬───────┘
                   │                 │        │
          ┌────────▼──────┐  ┌─────▼───┐ ┌──▼──────────┐
          │    MySQL       │  │  Redis  │ │    MinIO     │
          │  (用户数据)     │  │ (缓存)  │ │ (对象存储)   │
          └───────────────┘  └─────────┘ └─────────────┘

微服务划分

服务 职责 端口
Auth Service 用户注册/登录、JWT 签发 8081
File Service 文件上传/下载/列表/删除 8082
Traefik 反向代理 + SSL + 路由 80/443
MinIO S3 兼容对象存储(分布式存储学习核心) 9000

资源预估(2GB 内存可承受)

组件 内存限制 说明
k3s + Traefik ~350MB K8s 核心
MySQL 128MB 用户数据
Redis 64MB 会话缓存
MinIO 256MB 对象存储
Auth Service 64MB Go 编译后内存占用极低
File Service 64MB Go 编译后内存占用极低
系统 + Swap ~600MB 操作系统 + 2GB Swap 缓冲
合计 ~1.6GB 留有 400MB 余量

二、服务器环境搭建(从头开始)

2.1 系统初始化

# ===== 1. 更新系统 =====
sudo apt update && sudo apt upgrade -y

# ===== 2. 设置主机名 =====
sudo hostnamectl set-hostname cloudnest

# ===== 3. 关闭 SwapK8s 要求,后面用小 Swap 代替)=====
sudo swapoff -a
sudo sed -i '/swap/d' /etc/fstab

# ===== 4. 创建 2GB Swap 文件(K8s 1.28+ 允许有限 Swap=====
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# ===== 5. 配置 K8s 内核参数 =====
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sudo sysctl --system

# ===== 6. 安装基础工具 =====
sudo apt install -y curl wget git vim net-tools htop

2.2 安装 Docker

# ===== 安装 Docker =====
curl -fsSL https://get.docker.com | sudo sh

# ===== 将当前用户加入 docker 组(免 sudo=====
sudo usermod -aG docker $USER

# ===== 重新登录使生效(或执行)=====
newgrp docker

# ===== 验证 Docker =====
docker --version
docker run --rm hello-world

2.3 安装 k3s(轻量版 Kubernetes

# ===== 国内服务器使用 Rancher 中国镜像加速 =====
curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | \
  INSTALL_K3S_MIRROR=cn \
  K3S_KUBECONFIG_MODE="644" \
  sh -

# ===== 等待 k3s 启动完成 =====
sudo systemctl status k3s

# ===== 配置 kubectlk3s 自带)=====
sudo mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config

# ===== 验证集群状态 =====
kubectl get nodes
kubectl get pods -A

预期输出:节点状态为 ReadyTraefik 相关 Pod 正在运行。

2.4 安装 HelmK8s 包管理器)

# ===== 下载 Helm =====
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# ===== 验证 =====
helm version

三、部署基础设施(MySQL + Redis + MinIO

3.1 创建命名空间

kubectl create namespace cloudnest

3.2 部署 MySQL

创建文件 mysql-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
  namespace: cloudnest
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "cloudnest123"
        - name: MYSQL_DATABASE
          value: "cloudnest"
        ports:
        - containerPort: 3306
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
          requests:
            memory: "64Mi"
            cpu: "100m"
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-data
        hostPath:
          path: /data/mysql
          type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: cloudnest
spec:
  selector:
    app: mysql
  ports:
  - port: 3306
    targetPort: 3306
kubectl apply -f mysql-deployment.yaml
kubectl get pods -n cloudnest -w  # 等待 MySQL 就绪

3.3 部署 Redis

创建文件 redis-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
  namespace: cloudnest
spec:
  replicas: 1
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:7-alpine
        ports:
        - containerPort: 6379
        resources:
          limits:
            memory: "64Mi"
            cpu: "200m"
          requests:
            memory: "32Mi"
            cpu: "50m"
---
apiVersion: v1
kind: Service
metadata:
  name: redis
  namespace: cloudnest
spec:
  selector:
    app: redis
  ports:
  - port: 6379
    targetPort: 6379
kubectl apply -f redis-deployment.yaml

3.4 部署 MinIO(分布式存储核心)

创建文件 minio-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: minio
  namespace: cloudnest
spec:
  replicas: 1
  selector:
    matchLabels:
      app: minio
  template:
    metadata:
      labels:
        app: minio
    spec:
      containers:
      - name: minio
        image: minio/minio:latest
        args:
        - server
        - /data
        - --console-address
        - ":9001"
        env:
        - name: MINIO_ROOT_USER
          value: "minioadmin"
        - name: MINIO_ROOT_PASSWORD
          value: "minioadmin"
        ports:
        - containerPort: 9000
          name: api
        - containerPort: 9001
          name: console
        resources:
          limits:
            memory: "256Mi"
            cpu: "500m"
          requests:
            memory: "128Mi"
            cpu: "100m"
        volumeMounts:
        - name: minio-data
          mountPath: /data
      volumes:
      - name: minio-data
        hostPath:
          path: /data/minio
          type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: minio
  namespace: cloudnest
spec:
  selector:
    app: minio
  ports:
  - name: api
    port: 9000
    targetPort: 9000
  - name: console
    port: 9001
    targetPort: 9001
---
apiVersion: networking.k8s.io/v1
kind: IngressRoute
metadata:
  name: minio-console
  namespace: cloudnest
spec:
  entryPoints:
    - websecure
  routes:
  - match: Host(`minio.yourdomain.com`)
    kind: Rule
    services:
    - name: minio
      port: 9001
  tls:
    certResolver: letsencrypt
kubectl apply -f minio-deployment.yaml

# 验证 MinIO 就绪
kubectl get pods -n cloudnest

部署完成后,通过 https://minio.yourdomain.com 访问 MinIO 管理控制台。 用户名:minioadmin,密码:minioadmin

3.5 验证基础设施

# 查看所有 Pod 状态
kubectl get pods -n cloudnest

# 预期输出:
# NAME                    READY   STATUS    RESTARTS   AGE
# mysql-xxxxxxxxx-xxxxx   1/1     Running   0          2m
# redis-xxxxxxxxx-xxxxx   1/1     Running   0          1m
# minio-xxxxxxxxx-xxxxx    1/1     Running   0          1m

四、微服务开发(Golang

4.1 项目结构

cloudnest/
├── cmd/
│   ├── auth-service/
│   │   └── main.go          # 认证服务入口
│   └── file-service/
│       └── main.go          # 文件服务入口
├── internal/
│   ├── auth/
│   │   ├── handler.go       # HTTP 处理器
│   │   ├── model.go         # 数据模型
│   │   └── service.go       # 业务逻辑
│   ├── file/
│   │   ├── handler.go       # HTTP 处理器
│   │   ├── model.go         # 数据模型
│   │   └── service.go       # 业务逻辑
│   └── middleware/
│       └── jwt.go           # JWT 中间件
├── pkg/
│   ├── config/
│   │   └── config.go        # 配置管理
│   └── storage/
│       └── minio.go         # MinIO 客户端封装
├── deployments/
│   ├── k8s/
│   │   ├── auth-deployment.yaml
│   │   ├── file-deployment.yaml
│   │   └── ingress.yaml
│   └── docker/
│       ├── auth-service.Dockerfile
│       └── file-service.Dockerfile
├── go.mod
└── go.sum

4.2 初始化 Go 项目

# 在本地开发机执行
mkdir cloudnest && cd cloudnest
go mod init github.com/yourname/cloudnest

# 安装依赖
go get github.com/gin-gonic/gin
go get github.com/minio/minio-go/v7
go get github.com/golang-jwt/jwt/v5
go get github.com/go-sql-driver/mysql
go get github.com/redis/go-redis/v9
go get golang.org/x/crypto/bcrypt

4.3 配置管理 pkg/config/config.go

package config

import "os"

type Config struct {
    // Auth Service
    AuthPort      string
    JWTSecret     string
    MySQLDSN      string
    RedisAddr     string

    // File Service
    FilePort      string
    MinioEndpoint string
    MinioAccessKey string
    MinioSecretKey string
    MinioBucket   string
    MinioUseSSL   bool
}

func Load() *Config {
    return &Config{
        AuthPort:       getEnv("AUTH_PORT", "8081"),
        JWTSecret:      getEnv("JWT_SECRET", "cloudnest-secret-key"),
        MySQLDSN:       getEnv("MYSQL_DSN", "root:sun900120@tcp(mysql:3306)/cloudnest?charset=utf8mb4&parseTime=True"),
        RedisAddr:      getEnv("REDIS_ADDR", "redis:6379"),
        FilePort:       getEnv("FILE_PORT", "8082"),
        MinioEndpoint:  getEnv("MINIO_ENDPOINT", "minio:9000"),
        MinioAccessKey: getEnv("MINIO_ACCESS_KEY", "minioadmin"),
        MinioSecretKey: getEnv("MINIO_SECRET_KEY", "minioadmin"),
        MinioBucket:    getEnv("MINIO_BUCKET", "cloudnest-files"),
        MinioUseSSL:    false,
    }
}

func getEnv(key, fallback string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return fallback
}

4.4 Auth Service(认证服务)

cmd/auth-service/main.go

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/yourname/cloudnest/internal/auth"
    "github.com/yourname/cloudnest/pkg/config"
)

func main() {
    cfg := config.Load()

    r := gin.Default()

    // 初始化数据库连接
    auth.InitDB(cfg.MySQLDSN)
    auth.InitRedis(cfg.RedisAddr)

    // 路由
    auth.RegisterRoutes(r, cfg.JWTSecret)

    r.Run(":" + cfg.AuthPort)
}

internal/auth/model.go

package auth

import "time"

type User struct {
    ID        uint      `json:"id" gorm:"primaryKey"`
    Username  string    `json:"username" gorm:"uniqueIndex;size:50"`
    Password  string    `json:"-" gorm:"size:255"`
    CreatedAt time.Time `json:"created_at"`
}

type RegisterRequest struct {
    Username string `json:"username" binding:"required,min=3,max=50"`
    Password string `json:"password" binding:"required,min=6"`
}

type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

type TokenResponse struct {
    Token string `json:"token"`
}

internal/auth/service.go

package auth

import (
    "context"
    "time"

    "github.com/golang-jwt/jwt/v5"
    "github.com/redis/go-redis/v9"
    "golang.org/x/crypto/bcrypt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var (
    db    *gorm.DB
    rdb   *redis.Client
    ctx   = context.Background()
)

func InitDB(dsn string) {
    var err error
    db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database: " + err.Error())
    }
    db.AutoMigrate(&User{})
}

func InitRedis(addr string) {
    rdb = redis.NewClient(&redis.Options{Addr: addr})
}

func Register(username, password string) error {
    hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    return db.Create(&User{Username: username, Password: string(hashed)}).Error
}

func Login(username, password string) (string, error) {
    var user User
    if err := db.Where("username = ?", username).First(&user).Error; err != nil {
        return "", err
    }
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
        return "", err
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "user_id":  user.ID,
        "username": user.Username,
        "exp":      time.Now().Add(24 * time.Hour).Unix(),
    })
    return token.SignedString([]byte("cloudnest-secret-key"))
}

internal/auth/handler.go

package auth

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

func RegisterRoutes(r *gin.Engine, jwtSecret string) {
    r.POST("/register", handleRegister)
    r.POST("/login", handleLogin)
    r.GET("/me", authMiddleware(jwtSecret), handleMe)
}

func handleRegister(c *gin.Context) {
    var req RegisterRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    if err := Register(req.Username, req.Password); err != nil {
        c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
        return
    }
    c.JSON(http.StatusCreated, gin.H{"message": "注册成功"})
}

func handleLogin(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    token, err := Login(req.Username, req.Password)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
        return
    }
    c.JSON(http.StatusOK, TokenResponse{Token: token})
}

func handleMe(c *gin.Context) {
    claims, _ := c.Get("claims")
    c.JSON(http.StatusOK, claims)
}

func authMiddleware(jwtSecret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenStr := c.GetHeader("Authorization")
        if len(tokenStr) < 7 || tokenStr[:7] != "Bearer " {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
            c.Abort()
            return
        }
        token, err := jwt.Parse(tokenStr[7:], func(t *jwt.Token) (interface{}, error) {
            return []byte(jwtSecret), nil
        })
        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌无效"})
            c.Abort()
            return
        }
        c.Set("claims", token.Claims)
        c.Next()
    }
}

4.5 File Service(文件服务)

pkg/storage/minio.go

package storage

import (
    "context"
    "io"
    "time"

    "github.com/minio/minio-go/v7"
    "github.com/minio/minio-go/v7/pkg/credentials"
)

var client *minio.Client

func Init(endpoint, accessKey, secretKey string, useSSL bool) error {
    var err error
    client, err = minio.New(endpoint, &minio.Options{
        Creds:  credentials.NewStaticV4(accessKey, secretKey, ""),
        Secure: useSSL,
    })
    return err
}

func EnsureBucket(bucket string) error {
    ctx := context.Background()
    exists, err := client.BucketExists(ctx, bucket)
    if err != nil {
        return err
    }
    if !exists {
        return client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
    }
    return nil
}

func UploadFile(bucket, objectName string, reader io.Reader, size int64, contentType string) error {
    _, err := client.PutObject(context.Background(), bucket, objectName, reader, size,
        minio.PutObjectOptions{ContentType: contentType})
    return err
}

func GetPresignedURL(bucket, objectName string) (string, error) {
    url, err := client.PresignedGetObject(context.Background(), bucket, objectName, 24*3600*time.Second, nil)
    if err != nil {
        return "", err
    }
    return url.String(), nil
}

func ListFiles(bucket, prefix string) ([]string, error) {
    ctx := context.Background()
    var files []string
    objects := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{Prefix: prefix})
    for obj := range objects {
        if obj.Err != nil {
            return nil, obj.Err
        }
        files = append(files, obj.Key)
    }
    return files, nil
}

func DeleteFile(bucket, objectName string) error {
    return client.RemoveObject(context.Background(), bucket, objectName)
}

cmd/file-service/main.go

package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
    "github.com/yourname/cloudnest/pkg/config"
    "github.com/yourname/cloudnest/pkg/storage"
)

func main() {
    cfg := config.Load()

    // 初始化 MinIO
    if err := storage.Init(cfg.MinioEndpoint, cfg.MinioAccessKey, cfg.MinioSecretKey, cfg.MinioUseSSL); err != nil {
        panic("failed to connect minio: " + err.Error())
    }
    storage.EnsureBucket(cfg.MinioBucket)

    r := gin.Default()

    // 文件操作需要认证
    fileGroup := r.Group("/", authMiddleware(cfg.JWTSecret))
    {
        fileGroup.POST("/upload", handleUpload)
        fileGroup.GET("/files", handleList)
        fileGroup.GET("/download/:name", handleDownload)
        fileGroup.DELETE("/files/:name", handleDelete)
    }

    r.Run(":" + cfg.FilePort)
}

func authMiddleware(jwtSecret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenStr := c.GetHeader("Authorization")
        if len(tokenStr) < 7 || tokenStr[:7] != "Bearer " {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
            c.Abort()
            return
        }
        token, _ := jwt.Parse(tokenStr[7:], func(t *jwt.Token) (interface{}, error) {
            return []byte(jwtSecret), nil
        })
        if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
            c.Set("username", claims["username"])
        } else {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌无效"})
            c.Abort()
            return
        }
        c.Next()
    }
}

func handleUpload(c *gin.Context) {
    file, header, err := c.Request.FormFile("file")
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "文件上传失败"})
        return
    }
    defer file.Close()

    username := c.GetString("username")
    objectName := fmt.Sprintf("%s/%s", username, header.Filename)

    if err := storage.UploadFile("cloudnest-files", objectName, file, header.Size, header.Header.Get("Content-Type")); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "存储失败"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "message": "上传成功",
        "file":    objectName,
        "size":    header.Size,
    })
}

func handleList(c *gin.Context) {
    username := c.GetString("username")
    files, err := storage.ListFiles("cloudnest-files", username+"/")
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "获取文件列表失败"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"files": files})
}

func handleDownload(c *gin.Context) {
    username := c.GetString("username")
    name := c.Param("name")
    objectName := fmt.Sprintf("%s/%s", username, name)

    url, err := storage.GetPresignedURL("cloudnest-files", objectName)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"download_url": url})
}

func handleDelete(c *gin.Context) {
    username := c.GetString("username")
    name := c.Param("name")
    objectName := fmt.Sprintf("%s/%s", username, name)

    if err := storage.DeleteFile("cloudnest-files", objectName); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

4.6 Dockerfile

deployments/docker/auth-service.Dockerfile

# 多阶段构建 - 减小镜像体积
FROM golang:1.25.1-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o auth-service ./cmd/auth-service

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/auth-service .
EXPOSE 8081
CMD ["./auth-service"]

deployments/docker/file-service.Dockerfile

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o file-service ./cmd/file-service

FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/file-service .
EXPOSE 8082
CMD ["./file-service"]

五、K8s 部署微服务

5.1 构建并推送镜像

在本地开发机上执行(需要先安装 Docker):

# 进入项目根目录
cd cloudnest

# ===== 方案 A:推送到 Docker Hub(推荐)=====
# 1. 登录 Docker Hub
docker login

# 2. 构建镜像
docker build -f deployments/docker/auth-service.Dockerfile -t yourname/auth-service:v1 .
docker build -f deployments/docker/file-service.Dockerfile -t yourname/file-service:v1 .

# 3. 推送镜像
docker push yourname/auth-service:v1
docker push yourname/file-service:v1

# ===== 方案 B:直接在服务器上构建(无需 Docker Hub=====
# 将代码上传到服务器后,在服务器上执行:
# docker build -f deployments/docker/auth-service.Dockerfile -t auth-service:v1 .
# docker build -f deployments/docker/file-service.Dockerfile -t file-service:v1 .

5.2 部署 Auth Service

创建文件 deployments/k8s/auth-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: auth-service
  namespace: cloudnest
spec:
  replicas: 1
  selector:
    matchLabels:
      app: auth-service
  template:
    metadata:
      labels:
        app: auth-service
    spec:
      containers:
      - name: auth-service
        image: yourname/auth-service:v1
        # 如果在服务器本地构建,改为:
        # imagePullPolicy: Never
        # image: auth-service:v1
        ports:
        - containerPort: 8081
        env:
        - name: AUTH_PORT
          value: "8081"
        - name: JWT_SECRET
          value: "cloudnest-secret-key"
        - name: MYSQL_DSN
          value: "root:cloudnest123@tcp(mysql:3306)/cloudnest?charset=utf8mb4&parseTime=True"
        - name: REDIS_ADDR
          value: "redis:6379"
        resources:
          limits:
            memory: "64Mi"
            cpu: "200m"
          requests:
            memory: "32Mi"
            cpu: "50m"
---
apiVersion: v1
kind: Service
metadata:
  name: auth-service
  namespace: cloudnest
spec:
  selector:
    app: auth-service
  ports:
  - port: 8081
    targetPort: 8081

5.3 部署 File Service

创建文件 deployments/k8s/file-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: file-service
  namespace: cloudnest
spec:
  replicas: 1
  selector:
    matchLabels:
      app: file-service
  template:
    metadata:
      labels:
        app: file-service
    spec:
      containers:
      - name: file-service
        image: yourname/file-service:v1
        # 如果在服务器本地构建,改为:
        # imagePullPolicy: Never
        # image: file-service:v1
        ports:
        - containerPort: 8082
        env:
        - name: FILE_PORT
          value: "8082"
        - name: JWT_SECRET
          value: "cloudnest-secret-key"
        - name: MINIO_ENDPOINT
          value: "minio:9000"
        - name: MINIO_ACCESS_KEY
          value: "minioadmin"
        - name: MINIO_SECRET_KEY
          value: "minioadmin"
        - name: MINIO_BUCKET
          value: "cloudnest-files"
        resources:
          limits:
            memory: "64Mi"
            cpu: "200m"
          requests:
            memory: "32Mi"
            cpu: "50m"
---
apiVersion: v1
kind: Service
metadata:
  name: file-service
  namespace: cloudnest
spec:
  selector:
    app: file-service
  ports:
  - port: 8082
    targetPort: 8082

5.4 配置 Ingress(域名路由 + SSL

创建文件 deployments/k8s/ingress.yaml

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: cloudnest-ingress
  namespace: cloudnest
spec:
  entryPoints:
    - websecure
  routes:
  - match: Host(`api.yourdomain.com`) && PathPrefix(`/auth`)
    kind: Rule
    services:
    - name: auth-service
      port: 8081
  - match: Host(`api.yourdomain.com`) && PathPrefix(`/files`)
    kind: Rule
    services:
    - name: file-service
      port: 8082
  tls:
    certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: strip-prefix
  namespace: cloudnest
spec:
  stripPrefix:
    prefixes:
      - /auth
      - /files

5.5 配置 Let's Encrypt 自动证书

# 创建 Traefik 配置补丁,启用 Let's Encrypt
cat <<EOF | sudo tee /etc/rancher/k3s/config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    certResolvers:
      letsencrypt:
        email: your-email@example.com
        storage: /data/acme.json
        httpChallenge:
          entryPoint: web
    additionalArguments:
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
EOF

# 重启 k3s 使配置生效
sudo systemctl restart k3s

5.6 一键部署

# 在服务器上执行(将所有 YAML 文件上传到服务器后)

# 部署基础设施
kubectl apply -f mysql-deployment.yaml
kubectl apply -f redis-deployment.yaml
kubectl apply -f minio-deployment.yaml

# 等待基础设施就绪
kubectl wait --for=condition=ready pod -l app=mysql -n cloudnest --timeout=120s
kubectl wait --for=condition=ready pod -l app=redis -n cloudnest --timeout=60s
kubectl wait --for=condition=ready pod -l app=minio -n cloudnest --timeout=60s

# 部署微服务
kubectl apply -f deployments/k8s/auth-deployment.yaml
kubectl apply -f deployments/k8s/file-deployment.yaml
kubectl apply -f deployments/k8s/ingress.yaml

# 查看所有服务状态
kubectl get all -n cloudnest

六、域名配置

6.1 DNS 解析设置

在你的域名服务商(如阿里云、Cloudflare)添加以下 A 记录:

主机记录 记录类型 记录值
api A 你的服务器公网 IP
minio A 你的服务器公网 IP

6.2 防火墙开放端口

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 6443/tcp   # k3s API
sudo ufw allow 22/tcp     # SSH
sudo ufw enable

七、验证测试

7.1 检查集群状态

# 查看节点
kubectl get nodes

# 查看所有 Pod
kubectl get pods -n cloudnest

# 查看 Ingress
kubectl get ingressroute -n cloudnest

# 查看资源使用
kubectl top pods -n cloudnest

7.2 接口测试

# ===== 1. 注册用户 =====
curl -X POST https://api.yourdomain.com/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

# ===== 2. 登录获取 Token =====
curl -X POST https://api.yourdomain.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

# 记下返回的 token

# ===== 3. 上传文件 =====
curl -X POST https://api.yourdomain.com/files/upload \
  -H "Authorization: Bearer <你的token>" \
  -F "file=@/path/to/your/test.txt"

# ===== 4. 查看文件列表 =====
curl https://api.yourdomain.com/files/files \
  -H "Authorization: Bearer <你的token>"

# ===== 5. 下载文件 =====
curl https://api.yourdomain.com/files/download/test.txt \
  -H "Authorization: Bearer <你的token>"

7.3 MinIO 控制台

浏览器访问 https://minio.yourdomain.com,登录后可查看上传的文件。


八、日常运维命令速查

# 查看日志
kubectl logs -f deployment/auth-service -n cloudnest
kubectl logs -f deployment/file-service -n cloudnest

# 重启服务
kubectl rollout restart deployment/auth-service -n cloudnest
kubectl rollout restart deployment/file-service -n cloudnest

# 查看资源占用
kubectl top pods -n cloudnest
free -h    # 系统内存
df -h      # 磁盘使用

# 进入容器调试
kubectl exec -it deployment/auth-service -n cloudnest -- sh

# 查看 Service 详情
kubectl describe svc auth-service -n cloudnest

九、学习路线建议

阶段 学习目标 实操内容
第1周 熟悉 K8s 基础概念 部署 k3s,理解 Pod/Service/Deployment
第2周 微服务通信 理解 Service 发现,修改 Ingress 路由规则
第3周 分布式存储 学习 MinIO 操作,理解对象存储概念
第4周 持久化与配置 将 hostPath 改为 PVC,使用 ConfigMap/Secret
第5周 扩展练习 添加日志服务(Loki),监控(Prometheus

可扩展方向

  • 添加 Horizontal Pod Autoscaler:根据 CPU 自动扩缩容
  • 引入 ConfigMap/Secret:替代硬编码的配置和密码
  • 添加日志收集Loki + Grafana
  • 添加监控Prometheus + Grafana
  • 添加 CI/CDGitHub Actions 自动构建推送镜像
  • 数据库迁移hostPath → PVC 持久卷

附录:快速部署脚本

将以下脚本保存为 deploy-all.sh,一键部署所有基础设施:

#!/bin/bash
set -e

echo "=== CloudNest 一键部署脚本 ==="

# 创建命名空间
kubectl create namespace cloudnest 2>/dev/null || true

# 部署基础设施
echo "[1/3] 部署 MySQL..."
kubectl apply -f mysql-deployment.yaml

echo "[2/3] 部署 Redis..."
kubectl apply -f redis-deployment.yaml

echo "[3/3] 部署 MinIO..."
kubectl apply -f minio-deployment.yaml

echo "等待基础设施就绪..."
kubectl wait --for=condition=ready pod -l app=mysql -n cloudnest --timeout=120s
kubectl wait --for=condition=ready pod -l app=redis -n cloudnest --timeout=60s
kubectl wait --for=condition=ready pod -l app=minio -n cloudnest --timeout=60s

echo "=== 基础设施部署完成 ==="
echo "MySQL: mysql.cloudnest.svc.cluster.local:3306"
echo "Redis: redis.cloudnest.svc.cluster.local:6379"
echo "MinIO:  minio.cloudnest.svc.cluster.local:9000"
echo ""
echo "下一步:构建微服务镜像并部署"
echo "  kubectl apply -f deployments/k8s/auth-deployment.yaml"
echo "  kubectl apply -f deployments/k8s/file-deployment.yaml"
echo "  kubectl apply -f deployments/k8s/ingress.yaml"
chmod +x deploy-all.sh
./deploy-all.sh

提示:本文档中 yourdomain.comyournameyour-email@example.com 等占位符请替换为你的实际值。 所有密码仅用于学习环境,生产环境请使用 Secret 管理敏感信息。