iT邦幫忙

2025 iThome 鐵人賽

DAY 17
1
DevOps

賢者大叔的容器修煉手札系列 第 17

DevSpace Dependencies:微服務編排的藝術 🎭

  • 分享至 

  • xImage
  •  

賢者大叔的容器修煉手札系列 第 17 篇

DevSpace Dependencies:微服務編排的藝術 🎭

昨天我們學會了用 Commands 和 Hooks 打造自動化的資料庫環境,就像學會了開一家自動化餐廳的廚房!
但現實中的應用系統不只有資料庫,還需要:
🗄️ 資料庫服務 (PostgreSQL)
🚀 後端 API (Go 應用程式)
🌐 前端界面 (可選)
📊 監控系統 (可選)
這就像經營一家完整的餐廳,需要廚房、服務生、收銀台... 各部門協調運作!

今天我們要學習 DevSpace 的 Dependencies,讓微服務像交響樂團一樣和諧演奏!

✅ 核心學習目標

✅ Dependencies - 管理服務間的依賴關係
✅ Go 應用整合 - 連接資料庫的實戰應用
✅ 服務編排 - 確保啟動順序和依賴關係
✅ 完整工作流 - 從資料庫到 API 的端到端自動化

🏗️ 為什麼需要 Dependencies?

微服務的現實挑戰
想像你要開一家現代化餐廳:

https://ithelp.ithome.com.tw/upload/images/20250831/20104930pViswE4HhS.png

如果沒有好的編排機制:
🔴 順序混亂:廚師還沒到,服務生就開始接單
🔴 資源浪費:所有部門同時啟動,但廚房還沒準備好
🔴 錯誤連鎖:一個環節出問題,整個系統崩潰
🔴 調試困難:不知道是哪個服務出了問題

開發環境的類似困擾

# 傳統混亂流程 😫
kubectl apply -f postgres.yaml
kubectl apply -f go-app.yaml    # 💥 連不到資料庫!
kubectl apply -f frontend.yaml  # 💥 後端還沒準備好!

# 手動等待和重試...
kubectl get pods -w
kubectl logs go-app-xxx          # 查看錯誤
kubectl delete pod go-app-xxx    # 重啟應用
# 重複以上步驟... 😫

🎯 DevSpace Dependencies:服務編排指揮家

概念理解:像餐廳的開業流程
DevSpace Dependencies 就像餐廳的標準開業流程:
https://ithelp.ithome.com.tw/upload/images/20250831/20104930aKOwmSzhvm.png

基本語法結構

# devspace.yaml
version: v2beta1
name: my-microservices

dependencies:
  - name: database           # 依賴項目名稱
    source:
      path: ./database       # 本地路徑
    # 或者
    # source:
    #   git: https://github.com/user/database-config
    #   branch: main
    
  - name: backend
    source:
      path: ./backend
    dependencies:            # 這個服務依賴於 database
      - database

deployments:
  frontend:
    helm:
      chart:
        name: ./frontend
    dependencies:            # 前端依賴於後端
      - backend

🚀 實戰:Go + PostgreSQL 微服務架構

讓我們建立一個完整的 Go 微服務系統!

📈 系統架構圖
https://ithelp.ithome.com.tw/upload/images/20250901/2010493089JHrlQ0Oq.png

📁 專案結構設計

microservices-demo/
├── devspace.yaml                 # 主要編排配置
├── database/                     # 資料庫依賴
│   ├── devspace.yaml
│   ├── manifest/
│   │   ├── postgres-deployment.yaml
│   │   ├── postgres-pvc.yaml
│   │   └── postgres-service.yaml
│   └── sql/
│       ├── V1__Create_users_table.sql
│       └── V2__Add_init_data.sql
├── backend/                      # Go 後端服務
│   ├── devspace.yaml
│   ├── Dockerfile
│   ├── main.go
│   ├── go.mod
│   ├── go.sum
│   └── manifest/
│       ├── deployment.yaml
│       └── service.yaml
└── README.md

🗄️ 第一步:準備資料庫依賴

這部份能直接用昨天分享的內容,一字不改搬過來用

🚀 第二步:建立 Go 後端服務

package main

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

	"github.com/gorilla/mux"
	_ "github.com/lib/pq"
)

type User struct {
	ID         int       `json:"id"`
	Name       string    `json:"name"`
	Department string    `json:"department"`
	CreatedAt  time.Time `json:"created_at"`
}

type App struct {
	DB *sql.DB
}

func main() {
	app := &App{}

	// 連接資料庫
	if err := app.connectDB(); err != nil {
		log.Fatal("Failed to connect to database:", err)
	}
	defer app.DB.Close()

	// 設定路由
	router := mux.NewRouter()
	router.HandleFunc("/health", app.healthHandler).Methods("GET")
	router.HandleFunc("/users", app.getUsersHandler).Methods("GET")
	router.HandleFunc("/users", app.createUserHandler).Methods("POST")

	// 啟動服務器
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	log.Printf("🚀 Server starting on port %s", port)
	log.Fatal(http.ListenAndServe(":"+port, router))
}

func (app *App) connectDB() error {
	// 從環境變數獲取資料庫連接資訊
	host := os.Getenv("DB_HOST")
	if host == "" {
		host = "postgresql-service" // Kubernetes service name
	}

	port := os.Getenv("DB_PORT")
	if port == "" {
		port = "5432"
	}

	user := os.Getenv("DB_USER")
	if user == "" {
		user = "postgres"
	}

	password := os.Getenv("DB_PASSWORD")
	if password == "" {
		password = "secret"
	}

	dbname := os.Getenv("DB_NAME")
	if dbname == "" {
		dbname = "myapp"
	}

	// 建立conn string
	connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
		host, port, user, password, dbname)

	log.Printf("🔗 Connecting to database: %s:%s/%s", host, port, dbname)

	// 重試連接邏輯
	var db *sql.DB
	var err error

	for i := 0; i < 10; i++ {
		db, err = sql.Open("postgres", connStr)
		if err != nil {
			log.Printf("❌ Failed to open database connection (attempt %d): %v", i+1, err)
			time.Sleep(5 * time.Second)
			continue
		}

		err = db.Ping()
		if err != nil {
			log.Printf("❌ Failed to ping database (attempt %d): %v", i+1, err)
			time.Sleep(5 * time.Second)
			continue
		}

		log.Println("✅ Successfully connected to database")
		app.DB = db
		return nil
	}

	return fmt.Errorf("failed to connect to database after 10 attempts: %v", err)
}

func (app *App) healthHandler(w http.ResponseWriter, r *http.Request) {
	// 檢查資料庫連接
	err := app.DB.Ping()
	if err != nil {
		w.WriteHeader(http.StatusServiceUnavailable)
		json.NewEncoder(w).Encode(map[string]string{
			"status": "unhealthy",
			"error":  err.Error(),
		})
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"status":   "healthy",
		"database": "connected",
		"time":     time.Now().Format(time.RFC3339),
	})
}

func (app *App) getUsersHandler(w http.ResponseWriter, r *http.Request) {
	rows, err := app.DB.Query("SELECT id, name, department, created_at FROM users ORDER BY id")
	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, &user.Department, &user.CreatedAt)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		users = append(users, user)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(users)
}

func (app *App) createUserHandler(w http.ResponseWriter, r *http.Request) {
	var user User
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	query := "INSERT INTO users (name, department) VALUES ($1, $2) RETURNING id, created_at"
	err := app.DB.QueryRow(query, user.Name, user.Department).Scan(&user.ID, &user.CreatedAt)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(user)
}

backend/manifest/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: go-backend
  template:
    metadata:
      labels:
        app: go-backend
    spec:
      containers:
      - name: go-backend
        image: go-backend:latest
        ports:
        - containerPort: 8080
        env:
        - name: DB_HOST
          value: "postgresql-service"
        - name: DB_PORT
          value: "5432"
        - name: DB_USER
          value: "postgres"
        - name: DB_PASSWORD
          value: "password123"
        - name: DB_NAME
          value: "myapp"
        - name: PORT
          value: "8080"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

backend/devspace.yaml

version: v2beta1
name: backend

images:
  go-backend:
    image: go-backend
    dockerfile: ./Dockerfile

deployments:
  go-backend:
    kubectl:
      manifests:
        - manifest/deployment.yaml
        - manifest/service.yaml

hooks:
  # 部署前檢查 - 修正服務名稱和增加重試機制
  - name: pre-deploy-check
    events: ["before:deploy"]
    command: |
      echo "🚀 Starting Go backend deployment..."
      echo "🔍 Checking if database is ready..."
      
      # 等待資料庫服務可用 (重試機制)
      MAX_ATTEMPTS=12
      ATTEMPT=1
      
      while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
        echo "⏳ Attempt $ATTEMPT/$MAX_ATTEMPTS: Checking for database service..."
        
        # 檢查正確的服務名稱: postgresql-service
        if kubectl get svc postgresql-service -n devspace-demo >/dev/null 2>&1; then
          echo "✅ Database service 'postgresql-service' found!"
          
          # 額外檢查:確認資料庫 pod 是否就緒
          echo "🔍 Checking database pod status..."
          if kubectl get pods -l app=postgresql -n devspace-demo >/dev/null 2>&1; then
            POD_STATUS=$(kubectl get pods -l app=postgresql -n devspace-demo -o jsonpath='{.items[0].status.phase}' 2>/dev/null)
            if [ "$POD_STATUS" = "Running" ]; then
              echo "✅ Database pod is running!"
              
              # 測試資料庫連線
              DB_POD=$(kubectl get pods -l app=postgresql -n devspace-demo -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
              if [ ! -z "$DB_POD" ]; then
                echo "🔍 Testing database connectivity..."
                if kubectl exec $DB_POD -n devspace-demo -- pg_isready -U postgres >/dev/null 2>&1; then
                  echo "✅ Database is ready and accepting connections!"
                  break
                else
                  echo "⏳ Database pod exists but not ready yet..."
                fi
              fi
            else
              echo "⏳ Database pod status: $POD_STATUS"
            fi
          else
            echo "⏳ Database pod not found yet..."
          fi
        else
          echo "⏳ Database service not found yet..."
        fi
        
        if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then
          echo "❌ Database service not ready after $MAX_ATTEMPTS attempts"
          echo "🔍 Available services:"
          kubectl get svc -n devspace-demo
          echo "🔍 Available pods:"
          kubectl get pods -n devspace-demo
          exit 1
        fi
        
        echo "⏳ Waiting 10 seconds before retry..."
        sleep 10
        ATTEMPT=$((ATTEMPT + 1))
      done
      
      echo "✅ Database is ready! Proceeding with backend deployment..."

  # 部署後檢查
  - name: post-deploy-check
    events: ["after:deploy"]
    command: |
      echo "⏳ Waiting for Go backend to be ready..."
      kubectl wait --for=condition=ready pod -l app=go-backend -n devspace-demo --timeout=120s
      
      echo "🔍 Testing backend health..."
      sleep 10
      
      POD_NAME=$(kubectl get pods -l app=go-backend -n devspace-demo -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
      if [ ! -z "$POD_NAME" ]; then
        echo "✅ Backend pod found: $POD_NAME"
        
        # 測試健康檢查端點
        echo "🏥 Testing health endpoint..."
        kubectl exec $POD_NAME -n devspace-demo -- wget -q -O- http://localhost:8080/health 2>/dev/null && echo "✅ Health endpoint working!" || echo "⏳ Health endpoint will be available soon..."
        
        # 測試資料庫連線
        echo "🔍 Testing database connection from backend..."
        kubectl exec $POD_NAME -n devspace-demo -- wget -q -O- http://localhost:8080/users 2>/dev/null && echo "✅ Database connection working!" || echo "⏳ Database connection will be established soon..."
        
      else
        echo "❌ Backend pod not found"
      fi
      
      echo "✅ Backend deployment completed!"

# 開發環境配置
dev:
  go-backend:
    labelSelector:
      app: go-backend
    sync:
      - path: .:/app
        excludePaths:
          - '*.tmp'
          - '.git/'
          - 'vendor/'
          - 'go.sum'
    logs:
      enabled: true
    ports:
      - port: 8080:8080

# 實用命令
commands:
  test:
    description: "Test backend API endpoints"
    command: |
      echo "🧪 Testing Go Backend API..."
      
      POD_NAME=$(kubectl get pods -l app=go-backend -n devspace-demo -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
      
      if [ ! -z "$POD_NAME" ]; then
        echo "✅ Backend pod found: $POD_NAME"
        
        echo ""
        echo "🏥 Testing /health endpoint:"
        kubectl exec $POD_NAME -n devspace-demo -- curl -s http://localhost:8080/health || echo "❌ Health endpoint failed"
        
        echo ""
        echo "👥 Testing /users endpoint:"
        kubectl exec $POD_NAME -n devspace-demo -- curl -s http://localhost:8080/users || echo "❌ Users endpoint failed"
        
        echo ""
        echo "📊 Backend logs (last 20 lines):"
        kubectl logs $POD_NAME -n devspace-demo --tail=20
        
      else
        echo "❌ Backend pod not found"
      fi

  logs:
    description: "Show backend logs"
    command: |
      echo "📊 Go Backend Logs:"
      POD_NAME=$(kubectl get pods -l app=go-backend -n devspace-demo -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
      if [ ! -z "$POD_NAME" ]; then
        kubectl logs $POD_NAME -n devspace-demo -f
      else
        echo "❌ Backend pod not found"
      fi

  restart:
    description: "Restart backend pod"
    command: |
      echo "🔄 Restarting backend pod..."
      kubectl delete pod -l app=go-backend -n devspace-demo
      echo "✅ Pod deleted, Kubernetes will create a new one"

🎭 第三步:主要編排配置

根目錄的 devspace.yaml

version: v2beta1
name: microservices-demo

# 🎯 Dependencies (simplified structure)
dependencies:
  database:
    path: ./database
  backend:
    path: ./backend

# 📊 Commands only
commands:
  status:
    description: "Check status of all services"
    command: |
      echo "📊 Microservices Status Report"
      echo "================================"
      
      echo ""
      echo "🗄️ Database Status:"
      kubectl get pods -l app=postgresql
      
      echo ""
      echo "🚀 Backend Status:"
      kubectl get pods -l app=go-backend
      
      echo ""
      echo "🌐 Services:"
      kubectl get svc
      
  test-full:
    description: "Run full integration test"
    command: |
      echo "🧪 Running full integration test..."
      
      echo "1️⃣ Testing database..."
      kubectl get pods -l app=postgresql
      
      echo ""
      echo "2️⃣ Testing backend API..."
      kubectl get pods -l app=go-backend
      
      echo ""
      echo "3️⃣ Testing end-to-end flow..."
      
      # Port forward and test API
      kubectl port-forward svc/go-backend 8080:8080 &
      PF_PID=$!
      
      sleep 5
      
      echo "📊 Testing health endpoint:"
      curl -s http://localhost:8080/health || echo "❌ Health endpoint test failed"
      
      echo ""
      echo "👥 Testing users endpoint:"
      curl -s http://localhost:8080/users || echo "❌ Users endpoint test failed"
      
      # Cleanup
      kill $PF_PID 2>/dev/null || true
      
      echo ""
      echo "✅ Integration test completed!"
      
  clean:
    description: "Clean up all resources"
    command: |
      echo "🧹 Cleaning up microservices..."
      devspace purge
      kubectl delete pvc postgres-pvc 2>/dev/null || true
      echo "✅ Cleanup completed!"

# 🔧 Hooks
hooks:
  - name: startup-message
    events: ["before:deploy"]
    command: |
      echo "🎭 Starting Microservices Orchestra..."
      echo "📋 Services to deploy:"
      echo "  1. 🗄️ PostgreSQL Database"
      echo "  2. 🚀 Go Backend API"
      echo ""
      echo "⏳ This may take a few minutes for first-time setup..."

  - name: completion-message
    events: ["after:deploy"]
    command: |
      echo ""
      echo "🎉 Microservices deployment completed!"
      echo "================================"
      echo ""
      echo "🔗 Available endpoints:"
      echo "  • Database: localhost:5432 (manual port-forward needed)"
      echo "  • Backend API: localhost:8080 (manual port-forward needed)"
      echo ""
      echo "💡 Quick commands:"
      echo "  • devspace run status      - Check all services"
      echo "  • devspace run test-full   - Run integration tests"
      echo "  • devspace run clean       - Clean up everything"
      echo ""
      echo "🚀 Happy coding!"

https://ithelp.ithome.com.tw/upload/images/20250901/20104930dqOPZr76jx.png

🎬 實戰演練

  1. 啟動完整環境
# 在根目錄執行
devspace dev --debug

此時能觀察佈署流程的log

🔄 詳細執行步驟分析
階段 1: 初始化 (0-10秒)

🚀 devspace dev 啟動
├── 📦 檢查依賴關係 (database + backend)
├── 📂 檢查 SQL 文件 (3個文件確認)
└── 🏗️ 開始並行構建

階段 2: 資料庫部署 (10-30秒)

🗄️ Database Module:
├── 📦 部署 PVC + Deployment + Service
├── ⏳ 等待 Pod 調度完成
├── ⏳ 等待 Pod 就緒
├── 🔍 測試 PostgreSQL 連接
├── 📊 創建 myapp 資料庫
├── 📝 執行 3個 SQL 腳本
│   ├── V1__Create_users_table.sql ✅
│   ├── V2_Add_init_data.sql ✅
│   └── V3_add_more_user.sql ✅
└── 📊 顯示資料庫資訊

階段 3: 後端部署 (並行進行)

🔧 Backend Module:
├── 🏗️ 構建 Go Docker image
├── 📋 預部署檢查 (等待資料庫就緒)
├── 🚀 部署 Backend Deployment + Service
├── ⏳ 等待 Pod 就緒
├── 🏥 健康檢查 ✅
└── 🔍 測試資料庫連接 ✅

驗證一下 API,完美!

> curl http://localhost:20018/users

[{"id":1,"name":"雷N","department":"ithome","created_at":"2025-08-30T17:05:42.139438Z"},{"id":2,"name":"雷NN","department":"ithome","created_at":"2025-08-30T17:05:42.139438Z"},{"id":3,"name":"雷NNN","department":"ithome","created_at":"2025-08-30T17:05:42.139438Z"}]

🎯 總結

今天掌握了多個服務之間如果有依賴關係,需要按需啟動,則可以依賴 DevSpace 的 dependencies
且 dependencies 不只能指定相對路徑來調用服務,也能通過 Git 來取得,這是 Docker compose 所沒有的能力。

dependencies:
  api-server:
    git: https://github.com/my-api-server
    branch: stable
    pipeline: dev
  database-server:
    git: https://github.com/my-database-server
    tag: v3.0.1
    subPath: /configuration
    vars:
      ROOT_PASSWORD: ${ROOT_PASSWORD}

這給了新人同事或是團隊同事都很方便的統一方式,展示完整的微服務架構和組件關係。

https://ithelp.ithome.com.tw/upload/images/20250901/20104930mt8ELwuipO.png


上一篇
DevSpace Commands:建立自定義開發指令 📖
系列文
賢者大叔的容器修煉手札17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言