SiamCafe · Blog
Kustomize Overlay กับ Hexagonal Architecture —
บทความ

Kustomize Overlay กับ Hexagonal Architecture —

เผยแพร่ 28 พฤษภาคม 2569

Kustomize Overlay

Kustomize จัดการ Kubernetes YAML ไม่ใช้ Template Base + Overlay แยก Configuration ตาม Environment มาพร้อม kubectl kustomization.yaml Resources Patches

Hexagonal Architecture Ports and Adapters แยก Business Logic Infrastructure Ports Interface Adapters Implementation เปลี่ยน Infrastructure ไม่กระทบ Domain

Kustomize Structure

# === Kustomize Project Structure ===

# k8s/
# ├── base/                          # Base Configuration
# │   ├── kustomization.yaml
# │   ├── namespace.yaml
# │   ├── deployment.yaml
# │   ├── service.yaml
# │   ├── configmap.yaml
# │   └── hpa.yaml
# ├── overlays/
# │   ├── dev/                       # Development
# │   │   ├── kustomization.yaml
# │   │   ├── deployment-patch.yaml
# │   │   └── configmap-patch.yaml
# │   ├── staging/                   # Staging
# │   │   ├── kustomization.yaml
# │   │   ├── deployment-patch.yaml
# │   │   └── ingress.yaml
# │   └── production/                # Production
# │       ├── kustomization.yaml
# │       ├── deployment-patch.yaml
# │       ├── hpa-patch.yaml
# │       ├── ingress.yaml
# │       └── pdb.yaml
# └── components/                    # Reusable Components
#     ├── monitoring/
#     │   ├── kustomization.yaml
#     │   └── service-monitor.yaml
#     └── istio/
#         ├── kustomization.yaml
#         └── virtual-service.yaml

# base/kustomization.yaml
# apiVersion: kustomize.config.k8s.io/v1beta1
# kind: Kustomization
# namespace: my-app
# resources:
#   - namespace.yaml
#   - deployment.yaml
#   - service.yaml
#   - configmap.yaml
#   - hpa.yaml
# commonLabels:
#   app: my-app
#   managed-by: kustomize

# base/deployment.yaml
# apiVersion: apps/v1
# kind: Deployment
# metadata:
#   name: api-server
# spec:
#   replicas: 1
#   selector:
#     matchLabels:
#       app: api-server
#   template:
#     spec:
#       containers:
#       - name: api-server
#         image: my-app:latest
#         ports:
#         - containerPort: 8080
#         resources:
#           requests:
#             cpu: 100m
#             memory: 128Mi
#           limits:
#             cpu: 500m
#             memory: 512Mi

# overlays/production/kustomization.yaml
# apiVersion: kustomize.config.k8s.io/v1beta1
# kind: Kustomization
# namespace: my-app-prod
# bases:
#   - ../../base
# components:
#   - ../../components/monitoring
#   - ../../components/istio
# patches:
#   - path: deployment-patch.yaml
#   - path: hpa-patch.yaml
# images:
#   - name: my-app
#     newName: registry.example.com/my-app
#     newTag: v1.2.3
# resources:
#   - ingress.yaml
#   - pdb.yaml

# overlays/production/deployment-patch.yaml
# apiVersion: apps/v1
# kind: Deployment
# metadata:
#   name: api-server
# spec:
#   replicas: 3
#   template:
#     spec:
#       containers:
#       - name: api-server
#         resources:
#           requests:
#             cpu: 500m
#             memory: 512Mi
#           limits:
#             cpu: 2000m
#             memory: 2Gi

kustomize_commands = {
    "kubectl kustomize overlays/dev": "Preview dev configuration",
    "kubectl apply -k overlays/dev": "Apply dev configuration",
    "kubectl apply -k overlays/production": "Apply production",
    "kustomize build overlays/staging": "Build staging YAML",
    "kubectl diff -k overlays/production": "Diff before apply",
    "kustomize edit set image my-app=v1.3.0": "Update image tag",
}

print("Kustomize Commands:")
for cmd, desc in kustomize_commands.items():
    print(f"  {cmd}")
    print(f"    -> {desc}")

Hexagonal Architecture Pattern

# hexagonal.py — Hexagonal Architecture (Ports and Adapters)
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional

# === Domain Layer (Core Business Logic) ===
@dataclass
class Order:
    id: str
    customer_id: str
    items: List[dict]
    total: float
    status: str = "pending"

# === Ports (Interfaces) ===
class OrderRepository(ABC):
    """Port: Outbound — Database Access"""
    @abstractmethod
    def save(self, order: Order) -> bool: ...

    @abstractmethod
    def find_by_id(self, order_id: str) -> Optional[Order]: ...

    @abstractmethod
    def find_by_customer(self, customer_id: str) -> List[Order]: ...

class PaymentGateway(ABC):
    """Port: Outbound — Payment Processing"""
    @abstractmethod
    def charge(self, amount: float, customer_id: str) -> dict: ...

class NotificationService(ABC):
    """Port: Outbound — Notifications"""
    @abstractmethod
    def send(self, to: str, message: str) -> bool: ...

class OrderUseCase(ABC):
    """Port: Inbound — Application API"""
    @abstractmethod
    def create_order(self, customer_id: str, items: List[dict]) -> Order: ...

    @abstractmethod
    def get_order(self, order_id: str) -> Optional[Order]: ...

# === Application Service (Orchestration) ===
class OrderService(OrderUseCase):
    """Application Service — Orchestrates Ports"""

    def __init__(self, repo: OrderRepository,
                 payment: PaymentGateway,
                 notifier: NotificationService):
        self.repo = repo
        self.payment = payment
        self.notifier = notifier

    def create_order(self, customer_id: str, items: List[dict]) -> Order:
        total = sum(item["price"] * item["qty"] for item in items)
        order = Order(
            id=f"ORD-{customer_id[:4]}-001",
            customer_id=customer_id,
            items=items,
            total=total,
        )

        # Charge payment
        result = self.payment.charge(total, customer_id)
        if result["status"] == "success":
            order.status = "paid"
            self.repo.save(order)
            self.notifier.send(customer_id, f"Order {order.id} confirmed")
        else:
            order.status = "failed"

        return order

    def get_order(self, order_id: str) -> Optional[Order]:
        return self.repo.find_by_id(order_id)

# === Adapters (Implementations) ===
class PostgresOrderRepository(OrderRepository):
    """Adapter: PostgreSQL Database"""
    def save(self, order): print(f"  [PostgreSQL] Saved {order.id}"); return True
    def find_by_id(self, order_id): return None
    def find_by_customer(self, customer_id): return []

class StripePaymentGateway(PaymentGateway):
    """Adapter: Stripe Payment"""
    def charge(self, amount, customer_id):
        print(f"  [Stripe] Charged {amount} to {customer_id}")
        return {"status": "success", "transaction_id": "txn_123"}

class EmailNotificationService(NotificationService):
    """Adapter: Email Notification"""
    def send(self, to, message):
        print(f"  [Email] Sent to {to}: {message}")
        return True

# === Dependency Injection ===
repo = PostgresOrderRepository()
payment = StripePaymentGateway()
notifier = EmailNotificationService()

service = OrderService(repo, payment, notifier)

print("Hexagonal Architecture Demo:")
order = service.create_order("CUST-001", [
    {"name": "Widget", "price": 29.99, "qty": 2},
    {"name": "Gadget", "price": 49.99, "qty": 1},
])
print(f"  Order: {order.id} | Status: {order.status} | Total: {order.total}")

# แสดง Architecture Layers
layers = {
    "Domain": ["Order (Entity)", "Business Rules"],
    "Ports (Inbound)": ["OrderUseCase (API Interface)"],
    "Ports (Outbound)": ["OrderRepository", "PaymentGateway", "NotificationService"],
    "Adapters (Inbound)": ["REST API Controller", "gRPC Handler", "CLI"],
    "Adapters (Outbound)": ["PostgreSQL", "Stripe", "Email/SMS"],
}

print(f"\n\nHexagonal Architecture Layers:")
for layer, items in layers.items():
    print(f"  [{layer}]")
    for item in items:
        print(f"    - {item}")

Kustomize + Hexagonal Mapping

# kustomize_hexagonal.py — Mapping Hexagonal to K8s
mapping = {
    "Inbound Adapter: REST API": {
        "k8s_resource": "Deployment + Service + Ingress",
        "kustomize": "base/deployment.yaml + overlays/*/ingress.yaml",
        "port": "8080 (HTTP)",
    },
    "Inbound Adapter: gRPC": {
        "k8s_resource": "Deployment + Service (ClusterIP)",
        "kustomize": "base/grpc-service.yaml",
        "port": "50051 (gRPC)",
    },
    "Outbound Adapter: PostgreSQL": {
        "k8s_resource": "StatefulSet + PVC + Secret",
        "kustomize": "overlays/*/database-patch.yaml",
        "port": "5432",
    },
    "Outbound Adapter: Redis": {
        "k8s_resource": "Deployment + Service + ConfigMap",
        "kustomize": "components/redis/",
        "port": "6379",
    },
    "Outbound Adapter: Kafka": {
        "k8s_resource": "StatefulSet + Service + ConfigMap",
        "kustomize": "components/kafka/",
        "port": "9092",
    },
    "Cross-cutting: Monitoring": {
        "k8s_resource": "ServiceMonitor + PrometheusRule",
        "kustomize": "components/monitoring/",
        "port": "9090 (metrics)",
    },
}

print("Hexagonal -> Kubernetes Mapping:")
for adapter, info in mapping.items():
    print(f"\n  [{adapter}]")
    for key, value in info.items():
        print(f"    {key}: {value}")

# Environment Differences
env_diff = {
    "Development": {
        "replicas": 1,
        "resources": "100m CPU, 128Mi RAM",
        "database": "SQLite / Docker PostgreSQL",
        "cache": "ไม่มี หรือ In-memory",
        "ingress": "localhost:8080",
    },
    "Staging": {
        "replicas": 2,
        "resources": "250m CPU, 256Mi RAM",
        "database": "PostgreSQL (shared)",
        "cache": "Redis (shared)",
        "ingress": "staging.example.com",
    },
    "Production": {
        "replicas": "3-10 (HPA)",
        "resources": "500m-2000m CPU, 512Mi-2Gi RAM",
        "database": "PostgreSQL HA (Patroni)",
        "cache": "Redis Cluster",
        "ingress": "api.example.com + CDN",
    },
}

print(f"\n\nEnvironment Configuration:")
for env, config in env_diff.items():
    print(f"\n  [{env}]")
    for key, value in config.items():
        print(f"    {key}: {value}")

Best Practices

  • Base + Overlay: แยก Base Configuration กลาง Overlay เฉพาะ Environment
  • Components: ใช้ Kustomize Components สำหรับ Feature ที่ใช้ซ้ำ
  • Ports Interface: กำหนด Port เป็น Abstract Class ใน Domain Layer
  • Adapters แยก: แต่ละ Adapter เป็น Package แยก เปลี่ยนได้อิสระ
  • DI Container: ใช้ Dependency Injection เชื่อม Port กับ Adapter
  • GitOps: ใช้ Kustomize กับ Flux CD หรือ ArgoCD GitOps Workflow

Kustomize คืออะไร

จัดการ Kubernetes YAML ไม่ใช้ Template Base Overlay แยก Environment มาพร้อม kubectl kustomization.yaml Resources Patches Components