在上一篇文章中,我們成功使用 Helm 將 Nginx 部署到 GKE 上。這次,我們將更進一步,建立一個完整的三層架構應用:包含 MySQL 資料庫、Redis 快取服務,以及一個 Golang 後端應用。這個實作將更貼近實際的生產環境部署需求。
架構概述
我們將部署的架構包含:
- MySQL: 作為主要資料庫
- Redis: 作為快取和 Session 儲存
- Golang 應用: 連接 MySQL 和 Redis 的後端服務
這三個服務將透過 Kubernetes 的內部網路進行通訊,形成一個完整的微服務架構。
前置準備
延續上一篇文章的環境設定,確保您已經:
- 安裝並設定好
gcloud
、kubectl
、helm
- 建立並連接到 GKE Cluster
- 具備基本的 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
安全性考量
在生產環境中,建議進行以下安全性改善:
-
使用 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
-
設定網路政策:限制 Pod 之間的網路存取。
-
使用 RBAC:為應用程式設定適當的權限。
-
定期更新映像檔:確保使用最新的安全補丁。
清理資源
測試完成後,依序清理所有資源:
# 刪除 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 流程,實現自動化部署和更新。