在上一篇文章中,我們成功使用 Helm 將 Nginx 部署到 GKE 上。這次,我們將更進一步,建立一個完整的三層架構應用:包含 MySQL 資料庫、Redis 快取服務,以及一個 Golang 後端應用。這個實作將更貼近實際的生產環境部署需求。

架構概述

我們將部署的架構包含:

  • MySQL: 作為主要資料庫
  • Redis: 作為快取和 Session 儲存
  • Golang 應用: 連接 MySQL 和 Redis 的後端服務

這三個服務將透過 Kubernetes 的內部網路進行通訊,形成一個完整的微服務架構。

前置準備

延續上一篇文章的環境設定,確保您已經:

  1. 安裝並設定好 gcloudkubectlhelm
  2. 建立並連接到 GKE Cluster
  3. 具備基本的 Helm Chart 操作經驗

步驟一:部署 MySQL 資料庫

首先,我們使用 Helm 的官方 MySQL Chart 來部署資料庫。

新增 Bitnami Helm Repository

# 新增 Bitnami repository (包含高品質的 MySQL 和 Redis Charts)
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

建立 MySQL 設定檔

建立一個 mysql-values.yaml 檔案來客製化 MySQL 部署:

# mysql-values.yaml
auth:
  rootPassword: "rootpassword123"
  database: "myappdb"
  username: "appuser"
  password: "apppassword123"

primary:
  persistence:
    enabled: true
    storageClass: "standard-rwo"  # GKE 的預設儲存類別
    size: "10Gi"
  
  resources:
    requests:
      cpu: 250m
      memory: 256Mi
    limits:
      cpu: 500m
      memory: 512Mi

metrics:
  enabled: true  # 啟用監控指標

部署 MySQL

helm install mysql bitnami/mysql -f mysql-values.yaml

驗證 MySQL 部署

# 檢查 MySQL Pod 狀態
kubectl get pods -l app.kubernetes.io/name=mysql

# 測試連線 (在另一個終端機執行)
kubectl run mysql-client --rm --tty -i --restart='Never' \
  --image mysql:8.0 --command -- \
  mysql -h mysql -u appuser -papppassword123 -e "SHOW DATABASES;"

步驟二:部署 Redis 快取服務

接著部署 Redis 作為快取和 Session 儲存。

建立 Redis 設定檔

# redis-values.yaml
auth:
  enabled: true
  password: "redispassword123"

master:
  persistence:
    enabled: true
    storageClass: "standard-rwo"
    size: "8Gi"
  
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      cpu: 200m
      memory: 256Mi

replica:
  replicaCount: 1  # 建立一個讀取副本
  persistence:
    enabled: false  # 副本不需要持久化
  
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
    limits:
      cpu: 200m
      memory: 256Mi

metrics:
  enabled: true

部署 Redis

helm install redis bitnami/redis -f redis-values.yaml

驗證 Redis 部署

# 檢查 Redis Pod 狀態
kubectl get pods -l app.kubernetes.io/name=redis

# 測試 Redis 連線
kubectl run redis-client --rm --tty -i --restart='Never' \
  --env REDISCLI_AUTH=redispassword123 \
  --image redis:7.0 --command -- \
  redis-cli -h redis-master -p 6379 ping

步驟三:建立 Golang 應用

現在建立一個簡單的 Golang 應用來連接 MySQL 和 Redis。

建立 Golang 應用程式

首先,建立一個基本的 Golang 應用程式:

mkdir golang-app
cd golang-app

建立 main.go

// main.go
package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/go-redis/redis/v8"
    _ "github.com/go-sql-driver/mysql"
    "github.com/gorilla/mux"
    "golang.org/x/net/context"
)

type App struct {
    DB    *sql.DB
    Redis *redis.Client
}

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    app := &App{}
    
    // 初始化資料庫連線
    app.initDB()
    
    // 初始化 Redis 連線
    app.initRedis()
    
    // 建立路由
    router := mux.NewRouter()
    router.HandleFunc("/health", app.healthCheck).Methods("GET")
    router.HandleFunc("/users", app.getUsers).Methods("GET")
    router.HandleFunc("/users", app.createUser).Methods("POST")
    
    fmt.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", router))
}

func (app *App) initDB() {
    mysqlHost := os.Getenv("MYSQL_HOST")
    mysqlUser := os.Getenv("MYSQL_USER")
    mysqlPassword := os.Getenv("MYSQL_PASSWORD")
    mysqlDatabase := os.Getenv("MYSQL_DATABASE")
    
    dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", 
        mysqlUser, mysqlPassword, mysqlHost, mysqlDatabase)
    
    var err error
    app.DB, err = sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal("Failed to connect to database: ", err)
    }
    
    // 建立測試表格
    app.DB.Exec(`CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL
    )`)
}

func (app *App) initRedis() {
    redisHost := os.Getenv("REDIS_HOST")
    redisPassword := os.Getenv("REDIS_PASSWORD")
    
    app.Redis = redis.NewClient(&redis.Options{
        Addr:     redisHost + ":6379",
        Password: redisPassword,
        DB:       0,
    })
}

func (app *App) healthCheck(w http.ResponseWriter, r *http.Request) {
    // 檢查資料庫連線
    err := app.DB.Ping()
    if err != nil {
        http.Error(w, "Database connection failed", http.StatusInternalServerError)
        return
    }
    
    // 檢查 Redis 連線
    _, err = app.Redis.Ping(context.Background()).Result()
    if err != nil {
        http.Error(w, "Redis connection failed", http.StatusInternalServerError)
        return
    }
    
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
}

func (app *App) getUsers(w http.ResponseWriter, r *http.Request) {
    // 先嘗試從 Redis 取得快取
    ctx := context.Background()
    cached, err := app.Redis.Get(ctx, "users").Result()
    if err == nil {
        w.Header().Set("Content-Type", "application/json")
        w.Header().Set("X-Cache", "HIT")
        w.Write([]byte(cached))
        return
    }
    
    // 從資料庫查詢
    rows, err := app.DB.Query("SELECT id, name FROM users")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()
    
    var users []User
    for rows.Next() {
        var user User
        err := rows.Scan(&user.ID, &user.Name)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        users = append(users, user)
    }
    
    // 存入 Redis 快取 (30 秒過期)
    usersJSON, _ := json.Marshal(users)
    app.Redis.Set(ctx, "users", usersJSON, 30*time.Second)
    
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Cache", "MISS")
    json.NewEncoder(w).Encode(users)
}

func (app *App) createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    json.NewDecoder(r.Body).Decode(&user)
    
    result, err := app.DB.Exec("INSERT INTO users (name) VALUES (?)", user.Name)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    id, _ := result.LastInsertId()
    user.ID = int(id)
    
    // 清除快取
    app.Redis.Del(context.Background(), "users")
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

建立 go.mod

// go.mod
module golang-app

go 1.21

require (
    github.com/go-redis/redis/v8 v8.11.5
    github.com/go-sql-driver/mysql v1.7.1
    github.com/gorilla/mux v1.8.0
    golang.org/x/net v0.10.0
)

建立 Dockerfile

# Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/

COPY --from=builder /app/main .

EXPOSE 8080
CMD ["./main"]

建立並推送 Docker Image

# 建立 Docker Image (替換 PROJECT_ID 為您的 GCP 專案 ID)
docker build -t gcr.io/PROJECT_ID/golang-app:latest .

# 推送到 Google Container Registry
docker push gcr.io/PROJECT_ID/golang-app:latest

步驟四:建立 Golang 應用的 Helm Chart

為 Golang 應用建立一個 Helm Chart:

helm create golang-app-chart
cd golang-app-chart

修改 values.yaml

# values.yaml
replicaCount: 2

image:
  repository: gcr.io/PROJECT_ID/golang-app  # 替換為您的 PROJECT_ID
  pullPolicy: IfNotPresent
  tag: "latest"

service:
  type: LoadBalancer
  port: 80
  targetPort: 8080

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 200m
    memory: 256Mi

# 環境變數設定
env:
  - name: MYSQL_HOST
    value: "mysql"
  - name: MYSQL_USER
    value: "appuser"
  - name: MYSQL_PASSWORD
    value: "apppassword123"
  - name: MYSQL_DATABASE
    value: "myappdb"
  - name: REDIS_HOST
    value: "redis-master"
  - name: REDIS_PASSWORD
    value: "redispassword123"

# 健康檢查
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

修改 templates/deployment.yaml,加入環境變數:

# templates/deployment.yaml (在 containers 區塊中加入)
env:
{{- range .Values.env }}
- name: {{ .name }}
  value: {{ .value | quote }}
{{- end }}

# 加入健康檢查
livenessProbe:
  {{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
  {{- toYaml .Values.readinessProbe | nindent 12 }}

步驟五:部署 Golang 應用

# 返回到 golang-app-chart 的上一層目錄
cd ..

# 部署 Golang 應用
helm install golang-app ./golang-app-chart

驗證完整部署

檢查所有服務狀態

# 檢查所有 Pods
kubectl get pods

# 檢查所有 Services
kubectl get svc

# 檢查 Helm 部署
helm list

測試應用功能

# 取得 Golang 應用的外部 IP
kubectl get svc golang-app

# 測試健康檢查
curl http://EXTERNAL_IP/health

# 建立使用者
curl -X POST http://EXTERNAL_IP/users \
  -H "Content-Type: application/json" \
  -d '{"name": "John Doe"}'

# 取得使用者列表 (第一次會從資料庫查詢)
curl http://EXTERNAL_IP/users

# 再次取得使用者列表 (這次會從 Redis 快取取得)
curl http://EXTERNAL_IP/users

注意 HTTP 回應標頭中的 X-Cache 欄位,MISS 表示從資料庫查詢,HIT 表示從快取取得。

監控與維護

查看日誌

# 查看 Golang 應用日誌
kubectl logs -l app.kubernetes.io/name=golang-app-chart

# 查看 MySQL 日誌
kubectl logs -l app.kubernetes.io/name=mysql

# 查看 Redis 日誌
kubectl logs -l app.kubernetes.io/name=redis

擴展服務

# 擴展 Golang 應用到 3 個副本
kubectl scale deployment golang-app --replicas=3

# 或使用 Helm 升級
helm upgrade golang-app ./golang-app-chart --set replicaCount=3

安全性考量

在生產環境中,建議進行以下安全性改善:

  1. 使用 Kubernetes Secrets 管理敏感資料

    # 建立 MySQL 密碼 Secret
    kubectl create secret generic mysql-secret \
      --from-literal=password=apppassword123
    
    # 建立 Redis 密碼 Secret
    kubectl create secret generic redis-secret \
      --from-literal=password=redispassword123
    
  2. 設定網路政策:限制 Pod 之間的網路存取。

  3. 使用 RBAC:為應用程式設定適當的權限。

  4. 定期更新映像檔:確保使用最新的安全補丁。

清理資源

測試完成後,依序清理所有資源:

# 刪除 Helm 部署
helm uninstall golang-app
helm uninstall redis
helm uninstall mysql

# 刪除 PVC (如果需要)
kubectl delete pvc --all

# 刪除 Secrets (如果有建立)
kubectl delete secret mysql-secret redis-secret

透過這個完整的實作,我們成功建立了一個包含資料庫、快取和應用程式的三層架構系統。這個架構展示了如何在 Kubernetes 環境中部署和管理複雜的微服務應用,是邁向生產環境部署的重要一步。

在下一篇文章中,我們將探討如何為這個系統加入 CI/CD 流程,實現自動化部署和更新。