32 KiB
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. 关闭 Swap(K8s 要求,后面用小 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
# ===== 配置 kubectl(k3s 自带)=====
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
预期输出:节点状态为
Ready,Traefik 相关 Pod 正在运行。
2.4 安装 Helm(K8s 包管理器)
# ===== 下载 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/CD:GitHub 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.com、yourname、your-email@example.com等占位符请替换为你的实际值。 所有密码仅用于学习环境,生产环境请使用 Secret 管理敏感信息。