1321 lines
32 KiB
Markdown
1321 lines
32 KiB
Markdown
# 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 系统初始化
|
||
|
||
```bash
|
||
# ===== 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
|
||
|
||
```bash
|
||
# ===== 安装 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)
|
||
|
||
```bash
|
||
# ===== 国内服务器使用 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 包管理器)
|
||
|
||
```bash
|
||
# ===== 下载 Helm =====
|
||
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||
|
||
# ===== 验证 =====
|
||
helm version
|
||
```
|
||
|
||
---
|
||
|
||
## 三、部署基础设施(MySQL + Redis + MinIO)
|
||
|
||
### 3.1 创建命名空间
|
||
|
||
```bash
|
||
kubectl create namespace cloudnest
|
||
```
|
||
|
||
### 3.2 部署 MySQL
|
||
|
||
创建文件 `mysql-deployment.yaml`:
|
||
|
||
```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
|
||
```
|
||
|
||
```bash
|
||
kubectl apply -f mysql-deployment.yaml
|
||
kubectl get pods -n cloudnest -w # 等待 MySQL 就绪
|
||
```
|
||
|
||
### 3.3 部署 Redis
|
||
|
||
创建文件 `redis-deployment.yaml`:
|
||
|
||
```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
|
||
```
|
||
|
||
```bash
|
||
kubectl apply -f redis-deployment.yaml
|
||
```
|
||
|
||
### 3.4 部署 MinIO(分布式存储核心)
|
||
|
||
创建文件 `minio-deployment.yaml`:
|
||
|
||
```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
|
||
```
|
||
|
||
```bash
|
||
kubectl apply -f minio-deployment.yaml
|
||
|
||
# 验证 MinIO 就绪
|
||
kubectl get pods -n cloudnest
|
||
```
|
||
|
||
> 部署完成后,通过 `https://minio.yourdomain.com` 访问 MinIO 管理控制台。
|
||
> 用户名:`minioadmin`,密码:`minioadmin`
|
||
|
||
### 3.5 验证基础设施
|
||
|
||
```bash
|
||
# 查看所有 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 项目
|
||
|
||
```bash
|
||
# 在本地开发机执行
|
||
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`
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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`**
|
||
|
||
```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):
|
||
|
||
```bash
|
||
# 进入项目根目录
|
||
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`:
|
||
|
||
```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`:
|
||
|
||
```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`:
|
||
|
||
```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 自动证书
|
||
|
||
```bash
|
||
# 创建 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 一键部署
|
||
|
||
```bash
|
||
# 在服务器上执行(将所有 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 防火墙开放端口
|
||
|
||
```bash
|
||
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 检查集群状态
|
||
|
||
```bash
|
||
# 查看节点
|
||
kubectl get nodes
|
||
|
||
# 查看所有 Pod
|
||
kubectl get pods -n cloudnest
|
||
|
||
# 查看 Ingress
|
||
kubectl get ingressroute -n cloudnest
|
||
|
||
# 查看资源使用
|
||
kubectl top pods -n cloudnest
|
||
```
|
||
|
||
### 7.2 接口测试
|
||
|
||
```bash
|
||
# ===== 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`,登录后可查看上传的文件。
|
||
|
||
---
|
||
|
||
## 八、日常运维命令速查
|
||
|
||
```bash
|
||
# 查看日志
|
||
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`,一键部署所有基础设施:
|
||
|
||
```bash
|
||
#!/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"
|
||
```
|
||
|
||
```bash
|
||
chmod +x deploy-all.sh
|
||
./deploy-all.sh
|
||
```
|
||
|
||
---
|
||
|
||
> **提示**:本文档中 `yourdomain.com`、`yourname`、`your-email@example.com` 等占位符请替换为你的实际值。
|
||
> 所有密码仅用于学习环境,生产环境请使用 Secret 管理敏感信息。
|