微服務架構與容器化技術正在重新定義現代應用系統的建構方式。從 Netflix、Amazon 到阿里巴巴,全球領先的科技公司都已經擁抱微服務架構,並透過容器化技術實現敏捷的部署與彈性的擴展。微服務架構將龐大的單體應用拆解為多個小型、自治的服務,每個服務專注於單一業務能力,可以獨立開發、部署與擴展。這種架構模式不僅提升了系統的靈活性與可維護性,更使得團隊能夠採用不同的技術棧來解決特定問題。然而,微服務架構也帶來了新的挑戰,包括服務間通訊的複雜性、分散式系統的一致性問題、部署管理的複雜度等。容器化技術,特別是 Docker 與 Kubernetes 的出現,為這些挑戰提供了優雅的解決方案。Docker 將應用程式及其依賴封裝為標準化的容器,確保在任何環境中的一致性執行。Kubernetes 則提供了強大的容器編排能力,自動化處理容器的部署、擴展、監控與故障恢復。本文將深入探討微服務架構的設計原則與實踐方法,以及如何透過 Docker 與 Kubernetes 構建高可用、可擴展的雲端原生應用系統。

微服務架構設計原則與實踐

微服務架構的核心思想是將應用系統按照業務能力進行垂直拆分,每個微服務擁有獨立的資料儲存、業務邏輯與部署單元。這種架構模式源於面向服務架構(Service-Oriented Architecture, SOA)的演進,但更加強調服務的細粒度、自治性與去中心化治理。成功的微服務架構需要遵循一系列設計原則,包括單一職責原則、高內聚低耦合、API 優先設計、資料隔離等。

領域驅動設計(Domain-Driven Design, DDD)為微服務的拆分提供了理論基礎。透過識別限界上下文(Bounded Context),我們能夠確定服務的邊界,確保每個服務對應一個清晰的業務領域。聚合根(Aggregate Root)的概念幫助我們設計服務的資料模型,確保資料的一致性邊界。事件風暴(Event Storming)工作坊則是一種有效的協作方法,透過領域事件的識別來發現服務邊界與互動模式。

微服務架構的優勢是顯著的。技術異構性允許團隊為不同服務選擇最適合的技術棧,例如使用 Go 開發高效能的 API Gateway,使用 Python 實作機器學習服務,使用 Java Spring Boot 構建企業級應用。獨立部署能力使得服務的更新不會影響整個系統,大幅降低了變更的風險與部署的複雜度。服務的獨立擴展能力允許我們針對流量熱點進行精準的資源配置,避免了單體應用全局擴展的資源浪費。故障隔離確保單一服務的失敗不會導致整個系統崩潰,提升了系統的整體韌性。

然而,微服務架構也帶來了新的複雜性。分散式系統固有的挑戰,如網路延遲、部分失敗、時鐘不同步等問題需要妥善處理。服務間通訊需要選擇合適的協定與模式,RESTful API、gRPC、訊息佇列各有其適用場景。資料一致性從單體應用的 ACID 事務變為分散式環境的最終一致性,需要採用 Saga 模式、事件溯源等技術。運維複雜度顯著提升,需要完善的監控、日誌聚合、分散式追蹤等基礎設施支援。

from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from marshmallow import Schema, fields, ValidationError
import requests
import logging
from datetime import datetime
from functools import wraps
import jwt

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 帳戶管理微服務
account_app = Flask(__name__)
account_app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://user:pass@localhost/accounts'
account_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
account_app.config['JWT_SECRET_KEY'] = 'your-secret-key'

account_db = SQLAlchemy(account_app)

class Account(account_db.Model):
    """帳戶資料模型"""
    __tablename__ = 'accounts'
    
    id = account_db.Column(account_db.Integer, primary_key=True)
    username = account_db.Column(account_db.String(80), unique=True, nullable=False)
    email = account_db.Column(account_db.String(120), unique=True, nullable=False)
    password_hash = account_db.Column(account_db.String(255), nullable=False)
    created_at = account_db.Column(account_db.DateTime, default=datetime.utcnow)
    updated_at = account_db.Column(account_db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    is_active = account_db.Column(account_db.Boolean, default=True)

class AccountSchema(Schema):
    """帳戶序列化模式"""
    id = fields.Int(dump_only=True)
    username = fields.Str(required=True)
    email = fields.Email(required=True)
    password = fields.Str(required=True, load_only=True)
    created_at = fields.DateTime(dump_only=True)
    is_active = fields.Boolean()

account_schema = AccountSchema()
accounts_schema = AccountSchema(many=True)

def token_required(f):
    """JWT 驗證裝飾器"""
    @wraps(f)
    def decorated(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': 'Token is missing'}), 401
        
        try:
            token = token.split()[1]  # Remove 'Bearer ' prefix
            data = jwt.decode(
                token,
                account_app.config['JWT_SECRET_KEY'],
                algorithms=['HS256']
            )
            current_user = Account.query.get(data['user_id'])
            
            if not current_user:
                return jsonify({'error': 'Invalid token'}), 401
                
        except Exception as e:
            logger.error(f"Token validation error: {e}")
            return jsonify({'error': 'Invalid token'}), 401
        
        return f(current_user, *args, **kwargs)
    
    return decorated

@account_app.route('/health', methods=['GET'])
def health_check():
    """健康檢查端點"""
    try:
        # 測試資料庫連線
        account_db.session.execute('SELECT 1')
        return jsonify({
            'status': 'healthy',
            'service': 'account-service',
            'timestamp': datetime.utcnow().isoformat()
        }), 200
    except Exception as e:
        logger.error(f"Health check failed: {e}")
        return jsonify({
            'status': 'unhealthy',
            'error': str(e)
        }), 503

@account_app.route('/api/v1/accounts', methods=['POST'])
def create_account():
    """建立帳戶"""
    try:
        # 驗證請求資料
        account_data = account_schema.load(request.json)
        
        # 檢查使用者名稱是否已存在
        if Account.query.filter_by(username=account_data['username']).first():
            return jsonify({'error': 'Username already exists'}), 409
        
        # 檢查電子郵件是否已存在
        if Account.query.filter_by(email=account_data['email']).first():
            return jsonify({'error': 'Email already exists'}), 409
        
        # 建立新帳戶
        from werkzeug.security import generate_password_hash
        new_account = Account(
            username=account_data['username'],
            email=account_data['email'],
            password_hash=generate_password_hash(account_data['password'])
        )
        
        account_db.session.add(new_account)
        account_db.session.commit()
        
        # 發布帳戶建立事件
        publish_event('account.created', {
            'account_id': new_account.id,
            'username': new_account.username,
            'email': new_account.email
        })
        
        logger.info(f"Account created: {new_account.id}")
        
        return jsonify(account_schema.dump(new_account)), 201
        
    except ValidationError as e:
        return jsonify({'errors': e.messages}), 400
    except Exception as e:
        logger.error(f"Error creating account: {e}")
        account_db.session.rollback()
        return jsonify({'error': 'Internal server error'}), 500

@account_app.route('/api/v1/accounts/<int:account_id>', methods=['GET'])
@token_required
def get_account(current_user, account_id):
    """查詢帳戶"""
    try:
        account = Account.query.get(account_id)
        
        if not account:
            return jsonify({'error': 'Account not found'}), 404
        
        # 權限檢查:只能查詢自己的帳戶
        if current_user.id != account_id:
            return jsonify({'error': 'Unauthorized'}), 403
        
        return jsonify(account_schema.dump(account)), 200
        
    except Exception as e:
        logger.error(f"Error retrieving account: {e}")
        return jsonify({'error': 'Internal server error'}), 500

@account_app.route('/api/v1/accounts/<int:account_id>', methods=['PUT'])
@token_required
def update_account(current_user, account_id):
    """更新帳戶"""
    try:
        account = Account.query.get(account_id)
        
        if not account:
            return jsonify({'error': 'Account not found'}), 404
        
        if current_user.id != account_id:
            return jsonify({'error': 'Unauthorized'}), 403
        
        update_data = request.json
        
        # 更新允許的欄位
        if 'email' in update_data:
            if Account.query.filter(
                Account.email == update_data['email'],
                Account.id != account_id
            ).first():
                return jsonify({'error': 'Email already exists'}), 409
            account.email = update_data['email']
        
        account_db.session.commit()
        
        logger.info(f"Account updated: {account_id}")
        
        return jsonify(account_schema.dump(account)), 200
        
    except Exception as e:
        logger.error(f"Error updating account: {e}")
        account_db.session.rollback()
        return jsonify({'error': 'Internal server error'}), 500

@account_app.route('/api/v1/accounts/<int:account_id>', methods=['DELETE'])
@token_required
def delete_account(current_user, account_id):
    """刪除帳戶"""
    try:
        account = Account.query.get(account_id)
        
        if not account:
            return jsonify({'error': 'Account not found'}), 404
        
        if current_user.id != account_id:
            return jsonify({'error': 'Unauthorized'}), 403
        
        # 軟刪除
        account.is_active = False
        account_db.session.commit()
        
        # 發布帳戶刪除事件
        publish_event('account.deleted', {
            'account_id': account_id
        })
        
        logger.info(f"Account deleted: {account_id}")
        
        return jsonify({'message': 'Account deleted successfully'}), 200
        
    except Exception as e:
        logger.error(f"Error deleting account: {e}")
        account_db.session.rollback()
        return jsonify({'error': 'Internal server error'}), 500

@account_app.route('/api/v1/auth/login', methods=['POST'])
def login():
    """使用者登入"""
    try:
        data = request.json
        username = data.get('username')
        password = data.get('password')
        
        if not username or not password:
            return jsonify({'error': 'Username and password required'}), 400
        
        account = Account.query.filter_by(username=username).first()
        
        if not account or not account.is_active:
            return jsonify({'error': 'Invalid credentials'}), 401
        
        from werkzeug.security import check_password_hash
        if not check_password_hash(account.password_hash, password):
            return jsonify({'error': 'Invalid credentials'}), 401
        
        # 生成 JWT Token
        token = jwt.encode(
            {
                'user_id': account.id,
                'username': account.username,
                'exp': datetime.utcnow() + timedelta(hours=24)
            },
            account_app.config['JWT_SECRET_KEY'],
            algorithm='HS256'
        )
        
        logger.info(f"User logged in: {account.id}")
        
        return jsonify({
            'token': token,
            'user': account_schema.dump(account)
        }), 200
        
    except Exception as e:
        logger.error(f"Login error: {e}")
        return jsonify({'error': 'Internal server error'}), 500

def publish_event(event_type, event_data):
    """發布領域事件到訊息佇列"""
    try:
        # 這裡應該實作與訊息佇列(如 RabbitMQ, Kafka)的整合
        # 為簡化示例,僅記錄日誌
        logger.info(f"Event published: {event_type}, data: {event_data}")
        
        # 實際實作範例:
        # import pika
        # connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
        # channel = connection.channel()
        # channel.basic_publish(
        #     exchange='events',
        #     routing_key=event_type,
        #     body=json.dumps(event_data)
        # )
        # connection.close()
        
    except Exception as e:
        logger.error(f"Error publishing event: {e}")

# 預約服務微服務
from datetime import timedelta

booking_app = Flask(__name__)
booking_app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://user:pass@localhost/bookings'
booking_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

booking_db = SQLAlchemy(booking_app)

class Booking(booking_db.Model):
    """預約資料模型"""
    __tablename__ = 'bookings'
    
    id = booking_db.Column(booking_db.Integer, primary_key=True)
    account_id = booking_db.Column(booking_db.Integer, nullable=False)
    service_type = booking_db.Column(booking_db.String(100), nullable=False)
    booking_date = booking_db.Column(booking_db.Date, nullable=False)
    booking_time = booking_db.Column(booking_db.Time, nullable=False)
    status = booking_db.Column(
        booking_db.String(20),
        default='pending',
        nullable=False
    )  # pending, confirmed, cancelled, completed
    notes = booking_db.Column(booking_db.Text)
    created_at = booking_db.Column(booking_db.DateTime, default=datetime.utcnow)

class BookingSchema(Schema):
    """預約序列化模式"""
    id = fields.Int(dump_only=True)
    account_id = fields.Int(required=True)
    service_type = fields.Str(required=True)
    booking_date = fields.Date(required=True)
    booking_time = fields.Time(required=True)
    status = fields.Str()
    notes = fields.Str()
    created_at = fields.DateTime(dump_only=True)

booking_schema = BookingSchema()
bookings_schema = BookingSchema(many=True)

@booking_app.route('/health', methods=['GET'])
def booking_health_check():
    """健康檢查"""
    try:
        booking_db.session.execute('SELECT 1')
        return jsonify({
            'status': 'healthy',
            'service': 'booking-service',
            'timestamp': datetime.utcnow().isoformat()
        }), 200
    except Exception as e:
        return jsonify({
            'status': 'unhealthy',
            'error': str(e)
        }), 503

@booking_app.route('/api/v1/bookings/available-slots', methods=['GET'])
def get_available_slots():
    """查詢可用時段"""
    try:
        date_str = request.args.get('date')
        service_type = request.args.get('service_type')
        
        if not date_str:
            return jsonify({'error': 'Date parameter required'}), 400
        
        booking_date = datetime.strptime(date_str, '%Y-%m-%d').date()
        
        # 查詢該日期已預約的時段
        booked_slots = Booking.query.filter(
            Booking.booking_date == booking_date,
            Booking.status.in_(['pending', 'confirmed'])
        ).all()
        
        booked_times = {booking.booking_time for booking in booked_slots}
        
        # 生成可用時段(9:00-18:00, 每小時一個時段)
        available_slots = []
        start_time = datetime.strptime('09:00', '%H:%M').time()
        
        for hour in range(10):  # 9:00 to 18:00
            slot_time = (
                datetime.combine(booking_date, start_time) +
                timedelta(hours=hour)
            ).time()
            
            if slot_time not in booked_times:
                available_slots.append(slot_time.strftime('%H:%M'))
        
        return jsonify({
            'date': date_str,
            'available_slots': available_slots
        }), 200
        
    except ValueError as e:
        return jsonify({'error': 'Invalid date format'}), 400
    except Exception as e:
        logger.error(f"Error retrieving available slots: {e}")
        return jsonify({'error': 'Internal server error'}), 500

@booking_app.route('/api/v1/bookings', methods=['POST'])
def create_booking():
    """建立預約"""
    try:
        booking_data = booking_schema.load(request.json)
        
        # 驗證帳戶是否存在(呼叫帳戶服務)
        account_response = requests.get(
            f"http://account-service:5000/api/v1/accounts/{booking_data['account_id']}",
            headers={'Authorization': request.headers.get('Authorization')},
            timeout=5
        )
        
        if account_response.status_code != 200:
            return jsonify({'error': 'Invalid account'}), 400
        
        # 檢查時段是否可用
        existing_booking = Booking.query.filter(
            Booking.booking_date == booking_data['booking_date'],
            Booking.booking_time == booking_data['booking_time'],
            Booking.status.in_(['pending', 'confirmed'])
        ).first()
        
        if existing_booking:
            return jsonify({'error': 'Time slot not available'}), 409
        
        # 建立預約
        new_booking = Booking(**booking_data)
        booking_db.session.add(new_booking)
        booking_db.session.commit()
        
        logger.info(f"Booking created: {new_booking.id}")
        
        return jsonify(booking_schema.dump(new_booking)), 201
        
    except ValidationError as e:
        return jsonify({'errors': e.messages}), 400
    except requests.exceptions.RequestException as e:
        logger.error(f"Error communicating with account service: {e}")
        return jsonify({'error': 'Service unavailable'}), 503
    except Exception as e:
        logger.error(f"Error creating booking: {e}")
        booking_db.session.rollback()
        return jsonify({'error': 'Internal server error'}), 500

if __name__ == '__main__':
    # 僅供開發環境使用
    account_app.run(host='0.0.0.0', port=5000, debug=True)

這個完整的微服務範例展示了帳戶管理與預約系統兩個服務的實作。每個服務擁有獨立的資料庫、API 端點與業務邏輯。服務間透過 RESTful API 進行同步通訊,並透過事件發布實現非同步通訊。JWT 令牌機制提供了服務間的身份驗證,健康檢查端點支援容器編排平台的監控。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 140

package "前端層" {
  rectangle "Web 應用程式" as web
  rectangle "行動應用程式" as mobile
}

package "API 閘道層" {
  rectangle "API Gateway" as gateway {
    component "路由" as routing
    component "認證授權" as auth
    component "限流" as ratelimit
    component "監控" as monitor
  }
}

package "微服務層" {
  rectangle "帳戶服務" as account {
    component "API 層" as account_api
    database "帳戶資料庫" as account_db
  }
  
  rectangle "預約服務" as booking {
    component "API 層" as booking_api
    database "預約資料庫" as booking_db
  }
  
  rectangle "通知服務" as notification {
    component "API 層" as notification_api
    database "通知資料庫" as notification_db
  }
}

package "基礎設施層" {
  queue "訊息佇列" as mq
  rectangle "服務發現" as discovery
  rectangle "配置中心" as config
  rectangle "日誌聚合" as logging
  rectangle "監控系統" as monitoring
}

web -down-> gateway
mobile -down-> gateway

gateway -down-> account
gateway -down-> booking
gateway -down-> notification

account -down-> mq
booking -down-> mq
notification -down-> mq

account -down-> discovery
booking -down-> discovery
notification -down-> discovery

account -down-> config
booking -down-> config
notification -down-> config

account --> logging
booking --> logging
notification --> logging

gateway --> monitoring
account --> monitoring
booking --> monitoring
notification --> monitoring

@enduml

Docker 容器化完整實踐

Docker 透過容器技術實現了應用程式的標準化封裝與交付。相較於傳統虛擬機器,Docker 容器共享主機作業系統的核心,不需要完整的作業系統實例,因此啟動速度更快、資源佔用更少。容器提供了程序層級的隔離,透過 Linux Namespace 隔離程序空間、網路、檔案系統等資源,透過 Cgroups 限制 CPU、記憶體等資源使用。這種輕量級的虛擬化技術使得我們能夠在單一主機上運行數十甚至上百個容器。

Dockerfile 是定義容器映像的腳本檔案,它包含了一系列指令來描述如何構建映像。編寫高品質的 Dockerfile 需要遵循最佳實踐,包括使用官方基礎映像、減少映像層數、清理無用檔案、使用 .dockerignore 排除不需要的檔案等。多階段建置(Multi-stage Build)是優化映像大小的重要技術,它允許我們在建置階段使用完整的編譯環境,而在最終映像中只包含執行時所需的檔案。

# 多階段建置範例 - Python Flask 應用程式

# 建置階段
FROM python:3.11-slim as builder

WORKDIR /build

# 安裝建置依賴
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    gcc \
    python3-dev \
    libpq-dev && \
    rm -rf /var/lib/apt/lists/*

# 複製依賴檔案
COPY requirements.txt .

# 建立虛擬環境並安裝依賴
RUN python -m venv /opt/venv && \
    /opt/venv/bin/pip install --no-cache-dir --upgrade pip && \
    /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

# 執行階段
FROM python:3.11-slim

# 建立非 root 使用者
RUN useradd -m -u 1000 appuser && \
    mkdir -p /app && \
    chown -R appuser:appuser /app

WORKDIR /app

# 複製虛擬環境
COPY --from=builder /opt/venv /opt/venv

# 複製應用程式碼
COPY --chown=appuser:appuser . .

# 設定環境變數
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    FLASK_APP=app.py

# 切換到非 root 使用者
USER appuser

# 健康檢查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import requests; requests.get('http://localhost:5000/health')"

# 暴露連接埠
EXPOSE 5000

# 啟動應用程式
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--timeout", "60", "app:app"]
# Node.js 應用程式 Dockerfile 範例

FROM node:18-alpine as builder

WORKDIR /build

# 複製 package 檔案
COPY package*.json ./

# 安裝依賴(包含 devDependencies)
RUN npm ci

# 複製原始碼
COPY . .

# 建置應用程式
RUN npm run build

# 生產環境映像
FROM node:18-alpine

RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

WORKDIR /app

# 複製 package 檔案
COPY package*.json ./

# 只安裝生產依賴
RUN npm ci --only=production && \
    npm cache clean --force

# 從建置階段複製建置產物
COPY --from=builder --chown=nodejs:nodejs /build/dist ./dist

USER nodejs

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s \
    CMD node healthcheck.js

CMD ["node", "dist/server.js"]

Docker Compose 為多容器應用的定義與執行提供了便利的工具。透過 YAML 檔案,我們能夠聲明式地定義服務、網路、資料卷等資源,並透過單一命令啟動整個應用堆疊。這對於本地開發環境與測試環境特別有用,能夠快速重現完整的微服務架構。

# docker-compose.yml
# 微服務應用完整範例

version: '3.8'

services:
  
  # API Gateway
  api-gateway:
    image: nginx:alpine
    container_name: api-gateway
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - account-service
      - booking-service
    networks:
      - microservices-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
  
  # 帳戶服務
  account-service:
    build:
      context: ./account-service
      dockerfile: Dockerfile
    container_name: account-service
    environment:
      - DATABASE_URL=postgresql://postgres:password@account-db:5432/accounts
      - JWT_SECRET_KEY=${JWT_SECRET_KEY}
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      account-db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - microservices-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
  
  # 預約服務
  booking-service:
    build:
      context: ./booking-service
      dockerfile: Dockerfile
    container_name: booking-service
    environment:
      - DATABASE_URL=postgresql://postgres:password@booking-db:5432/bookings
      - ACCOUNT_SERVICE_URL=http://account-service:5000
      - RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672/
    depends_on:
      booking-db:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - microservices-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
  
  # 帳戶資料庫
  account-db:
    image: postgres:15-alpine
    container_name: account-db
    environment:
      - POSTGRES_DB=accounts
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - account-db-data:/var/lib/postgresql/data
      - ./account-service/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - microservices-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  # 預約資料庫
  booking-db:
    image: postgres:15-alpine
    container_name: booking-db
    environment:
      - POSTGRES_DB=bookings
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - booking-db-data:/var/lib/postgresql/data
      - ./booking-service/init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - microservices-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  # Redis 快取
  redis:
    image: redis:7-alpine
    container_name: redis
    command: redis-server --appendonly yes --requirepass redispassword
    volumes:
      - redis-data:/data
    networks:
      - microservices-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  # RabbitMQ 訊息佇列
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: rabbitmq
    environment:
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
    ports:
      - "15672:15672"  # Management UI
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    networks:
      - microservices-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3
  
  # Prometheus 監控
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    networks:
      - microservices-network
    restart: unless-stopped
  
  # Grafana 視覺化
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
    ports:
      - "3000:3000"
    depends_on:
      - prometheus
    networks:
      - microservices-network
    restart: unless-stopped

# 網路定義
networks:
  microservices-network:
    driver: bridge

# 資料卷定義
volumes:
  account-db-data:
    driver: local
  booking-db-data:
    driver: local
  redis-data:
    driver: local
  rabbitmq-data:
    driver: local
  prometheus-data:
    driver: local
  grafana-data:
    driver: local

常用的 Docker 命令涵蓋了映像管理、容器操作、網路配置、資料卷管理等多個面向。掌握這些命令對於日常的開發與除錯工作至關重要。映像建置使用 docker build,容器啟動使用 docker run,查看容器日誌使用 docker logs,進入容器除錯使用 docker exec。Docker 的分層檔案系統使得映像的儲存與傳輸非常高效,每一層都可以被快取與重用。

容器安全是不可忽視的議題。使用非 root 使用者執行容器程序、掃描映像漏洞、限制容器資源、使用只讀檔案系統、定期更新基礎映像等都是重要的安全實踐。Docker Content Trust 提供了映像簽章與驗證機制,確保映像的完整性與來源可信。

Kubernetes 容器編排深度實踐

Kubernetes 已成為容器編排的事實標準,提供了聲明式的 API 來定義應用的期望狀態,並自動化地確保實際狀態與期望狀態一致。Kubernetes 的架構採用主從模式,控制平面包含 API Server、Scheduler、Controller Manager 與 etcd 分散式鍵值儲存,工作節點則運行 Kubelet、Kube-proxy 與容器執行時。

Pod 是 Kubernetes 的最小部署單元,通常包含一個或多個緊密耦合的容器。這些容器共享網路命名空間與儲存資料卷,能夠透過 localhost 互相通訊。Pod 的設計模式包括 Sidecar(輔助容器)、Ambassador(代理容器)、Adapter(轉接器容器)等,這些模式為解決橫切關注點提供了優雅的方案。

# Kubernetes 部署配置範例

# Namespace 定義
apiVersion: v1
kind: Namespace
metadata:
  name: microservices
  labels:
    name: microservices
    environment: production

---
# ConfigMap - 應用配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: microservices
data:
  APP_ENV: "production"
  LOG_LEVEL: "info"
  REDIS_HOST: "redis-service"
  RABBITMQ_HOST: "rabbitmq-service"

---
# Secret - 敏感資訊
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
  namespace: microservices
type: Opaque
stringData:
  JWT_SECRET_KEY: "your-secret-key-here"
  DATABASE_PASSWORD: "your-database-password"
  REDIS_PASSWORD: "your-redis-password"

---
# 帳戶服務 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: account-service
  namespace: microservices
  labels:
    app: account-service
    version: v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: account-service
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: account-service
        version: v1
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "5000"
        prometheus.io/path: "/metrics"
    spec:
      serviceAccountName: account-service-sa
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
      - name: account-service
        image: your-registry.com/account-service:v1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 5000
          protocol: TCP
        env:
        - name: APP_ENV
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: APP_ENV
        - name: DATABASE_URL
          value: "postgresql://postgres:$(DATABASE_PASSWORD)@postgres-service:5432/accounts"
        - name: JWT_SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: JWT_SECRET_KEY
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: DATABASE_PASSWORD
        resources:
          requests:
            cpu: 250m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /health
            port: 5000
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /health
            port: 5000
          initialDelaySeconds: 20
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
        volumeMounts:
        - name: app-logs
          mountPath: /app/logs
      volumes:
      - name: app-logs
        emptyDir: {}

---
# 帳戶服務 Service
apiVersion: v1
kind: Service
metadata:
  name: account-service
  namespace: microservices
  labels:
    app: account-service
spec:
  type: ClusterIP
  selector:
    app: account-service
  ports:
  - name: http
    port: 80
    targetPort: 5000
    protocol: TCP
  sessionAffinity: None

---
# 水平自動擴展
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: account-service-hpa
  namespace: microservices
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: account-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 30
      - type: Pods
        value: 2
        periodSeconds: 30
      selectPolicy: Max

---
# Ingress - 流量入口
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: microservices-ingress
  namespace: microservices
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls-cert
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /api/v1/accounts
        pathType: Prefix
        backend:
          service:
            name: account-service
            port:
              number: 80
      - path: /api/v1/bookings
        pathType: Prefix
        backend:
          service:
            name: booking-service
            port:
              number: 80

Service 是 Kubernetes 中服務發現與負載平衡的抽象,它為一組 Pod 提供穩定的網路端點。ClusterIP Service 僅在叢集內部可訪問,NodePort Service 在每個節點上暴露固定連接埠,LoadBalancer Service 整合雲端供應商的負載平衡器。Ingress 則提供了 HTTP/HTTPS 層的路由功能,支援基於域名與路徑的流量分發。

ConfigMap 與 Secret 分別用於管理配置資料與敏感資訊。ConfigMap 儲存非敏感的配置,如應用參數、環境變數等,而 Secret 則用於儲存密碼、金鑰、證書等敏感資料,並支援加密儲存。這種外部化配置的模式使得應用映像可以在不同環境中重用,僅需更換配置即可。

Kubernetes 的自動擴展能力是其強大之處。水平自動擴展(Horizontal Pod Autoscaler)根據 CPU、記憶體或自定義指標自動調整 Pod 數量,垂直自動擴展(Vertical Pod Autoscaler)則調整 Pod 的資源請求與限制。叢集自動擴展(Cluster Autoscaler)在節點資源不足時自動增加節點,在資源閒置時自動縮減節點。

滾動更新與藍綠部署是 Kubernetes 支援的部署策略。滾動更新逐步替換舊版本 Pod,確保服務持續可用。藍綠部署則同時運行新舊兩個版本,驗證新版本正常後切換流量。金絲雀部署先將少量流量導向新版本,逐步增加比例,降低全面部署的風險。這些策略配合健康檢查機制,能夠實現零停機時間的部署。

玄貓認為,微服務架構與容器化技術的結合代表了現代應用系統建構的最佳實踐。這種架構模式雖然增加了系統的複雜度,但帶來的靈活性、可擴展性與韌性是傳統單體應用無法比擬的。Docker 簡化了應用的封裝與交付,Kubernetes 則提供了強大的編排能力。然而,成功採用這些技術需要團隊具備分散式系統的知識、容器技術的經驗,以及 DevOps 文化的支持。從單體應用遷移到微服務是一個漸進的過程,需要審慎規劃服務邊界、設計良好的 API、建立完善的監控與日誌體系。只有當組織的技術能力與文化準備就緒時,微服務架構才能真正發揮其價值,而非成為新的技術債務來源。