Files
cloudnest/CloudNest-部署指南.md

1321 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 关闭 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
```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
# ===== 配置 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
```
> **预期输出**:节点状态为 `Ready`Traefik 相关 Pod 正在运行。
### 2.4 安装 HelmK8s 包管理器)
```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 管理敏感信息。