SiamCafe · Blog
Medusa Commerce GitOps Workflow — Deploy E-commerce ด้วย ArgoCD
บทความ

Medusa Commerce GitOps Workflow — Deploy E-commerce ด้วย ArgoCD

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

Medusa Commerce คืออะไรและเหมาะกับธุรกิจแบบไหน

Medusa เป็น Open Source headless commerce platform ที่พัฒนาด้วย Node.js/TypeScript ออกแบบมาให้เป็นทางเลือกแทน Shopify สำหรับธุรกิจที่ต้องการควบคุมระบบ e-commerce ของตัวเองอย่างเต็มที่ Medusa แยก backend (API) ออกจาก frontend ทำให้สามารถใช้ frontend framework อะไรก็ได้เช่น Next.js, Gatsby หรือ Nuxt

Medusa มีฟีเจอร์หลักครบถ้วนสำหรับ e-commerce ได้แก่ Product Management สำหรับจัดการสินค้าและ variants, Order Management สำหรับจัดการคำสั่งซื้อ, Cart และ Checkout flow ที่ปรับแต่งได้, Payment Integration รองรับ Stripe PayPal และอื่นๆ, Shipping Integration รองรับหลาย fulfillment providers, Multi-region support สำหรับขายหลายประเทศ และ Plugin System สำหรับเพิ่มฟังก์ชัน

GitOps เป็นแนวทางการจัดการ infrastructure และ application deployment โดยใช้ Git เป็น single source of truth ทุกการเปลี่ยนแปลงต้องผ่าน Git commit และ Pull Request ทำให้มี audit trail ที่ชัดเจน สามารถ rollback ได้ง่าย และทีมทำงานร่วมกันได้ดี

การรวม Medusa กับ GitOps ช่วยให้การ deploy e-commerce platform เป็นไปอย่างเป็นระบบ ทุก config change ถูกบันทึกใน Git สามารถ review ก่อน apply และ rollback ได้ทันทีถ้ามีปัญหา

ติดตั้ง Medusa Commerce ด้วย Docker Compose

ตั้งค่า Medusa Backend พร้อม PostgreSQL และ Redis

# docker-compose.yml — Medusa Commerce Stack
version: '3.8'

services:
 postgres:
 image: postgres:16-alpine
 environment:
 POSTGRES_USER: medusa
 POSTGRES_PASSWORD: medusa_db_pass
 POSTGRES_DB: medusa_db
 volumes:
 - pgdata:/var/lib/postgresql/data
 ports:
 - "5432:5432"
 healthcheck:
 test: ["CMD-SHELL", "pg_isready -U medusa"]
 interval: 10s
 timeout: 5s
 retries: 5

 redis:
 image: redis:7-alpine
 ports:
 - "6379:6379"
 volumes:
 - redisdata:/data
 healthcheck:
 test: ["CMD", "redis-cli", "ping"]
 interval: 10s
 timeout: 5s
 retries: 5

 medusa-backend:
 build:
 context: ./backend
 dockerfile: Dockerfile
 environment:
 NODE_ENV: production
 DATABASE_URL: postgres://medusa:medusa_db_pass@postgres:5432/medusa_db
 REDIS_URL: redis://redis:6379
 JWT_SECRET: your-jwt-secret-key-here
 COOKIE_SECRET: your-cookie-secret-key-here
 STORE_CORS: "http://localhost:8000, https://store.example.com"
 ADMIN_CORS: "http://localhost:7001, https://admin.example.com"
 MEDUSA_ADMIN_ONBOARDING_TYPE: default
 ports:
 - "9000:9000"
 depends_on:
 postgres:
 condition: service_healthy
 redis:
 condition: service_healthy
 command: sh -c "npx medusa migrations run && npx medusa start"

 medusa-admin:
 image: node:20-alpine
 working_dir: /app
 volumes:
 - ./admin:/app
 ports:
 - "7001:7001"
 command: npm run dev
 depends_on:
 - medusa-backend

volumes:
 pgdata:
 redisdata:

# สร้าง Medusa Backend
# npx create-medusa-app@latest my-store
# cd my-store

# Dockerfile สำหรับ Medusa Backend
cat > backend/Dockerfile << 'DOCKERFILE'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 9000
CMD ["npx", "medusa", "start"]
DOCKERFILE

# docker compose up -d
# ทดสอบ: curl http://localhost:9000/health
# Admin: http://localhost:7001

ตั้งค่า GitOps Workflow ด้วย ArgoCD

ตั้งค่า ArgoCD สำหรับ GitOps deployment ของ Medusa Commerce

# ติดตั้ง ArgoCD บน Kubernetes
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# รอ ArgoCD พร้อม
kubectl wait --for=condition=available deployment/argocd-server -n argocd --timeout=300s

# ดึง admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Kubernetes Manifests สำหรับ Medusa
# k8s/base/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
 name: medusa-commerce

---
# k8s/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: medusa-backend
 namespace: medusa-commerce
spec:
 replicas: 2
 selector:
 matchLabels:
 app: medusa-backend
 template:
 metadata:
 labels:
 app: medusa-backend
 spec:
 containers:
 - name: medusa
 image: myregistry/medusa-backend:v1.0.0
 ports:
 - containerPort: 9000
 envFrom:
 - secretRef:
 name: medusa-secrets
 - configMapRef:
 name: medusa-config
 resources:
 requests:
 cpu: 250m
 memory: 512Mi
 limits:
 cpu: 1000m
 memory: 1Gi
 readinessProbe:
 httpGet:
 path: /health
 port: 9000
 initialDelaySeconds: 30
 periodSeconds: 10
 livenessProbe:
 httpGet:
 path: /health
 port: 9000
 initialDelaySeconds: 60
 periodSeconds: 30

---
# k8s/base/service.yaml
apiVersion: v1
kind: Service
metadata:
 name: medusa-backend
 namespace: medusa-commerce
spec:
 selector:
 app: medusa-backend
 ports:
 - port: 9000
 targetPort: 9000

---
# k8s/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
 name: medusa-config
 namespace: medusa-commerce
data:
 NODE_ENV: "production"
 STORE_CORS: "https://store.example.com"
 ADMIN_CORS: "https://admin.example.com"

---
# ArgoCD Application
# k8s/argocd/medusa-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
 name: medusa-commerce
 namespace: argocd
spec:
 project: default
 source:
 repoURL: https://github.com/myorg/medusa-k8s.git
 targetRevision: main
 path: k8s/overlays/production
 destination:
 server: https://kubernetes.default.svc
 namespace: medusa-commerce
 syncPolicy:
 automated:
 prune: true
 selfHeal: true
 syncOptions:
 - CreateNamespace=true
 retry:
 limit: 3
 backoff:
 duration: 5s
 maxDuration: 3m

# kubectl apply -f k8s/argocd/medusa-app.yaml

สร้าง CI/CD Pipeline สำหรับ Medusa

GitHub Actions Pipeline สำหรับ build, test และ deploy Medusa

# .github/workflows/medusa-ci-cd.yml
name: Medusa Commerce CI/CD

on:
 push:
 branches: [main, develop]
 pull_request:
 branches: [main]

env:
 REGISTRY: ghcr.io
 IMAGE_NAME: }/medusa-backend

jobs:
 test:
 runs-on: ubuntu-latest
 services:
 postgres:
 image: postgres:16-alpine
 env:
 POSTGRES_USER: test
 POSTGRES_PASSWORD: test
 POSTGRES_DB: medusa_test
 ports: ["5432:5432"]
 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
 redis:
 image: redis:7-alpine
 ports: ["6379:6379"]

 steps:
 - uses: actions/checkout@v4
 - uses: actions/setup-node@v4
 with:
 node-version: '20'
 cache: 'npm'

 - run: npm ci
 - run: npm run build

 - name: Run tests
 env:
 DATABASE_URL: postgres://test:test@localhost:5432/medusa_test
 REDIS_URL: redis://localhost:6379
 JWT_SECRET: test-secret
 COOKIE_SECRET: test-cookie
 run: |
 npx medusa migrations run
 npm test

 build-and-push:
 needs: test
 runs-on: ubuntu-latest
 if: github.ref == 'refs/heads/main'
 outputs:
 image_tag: }

 steps:
 - uses: actions/checkout@v4
 - uses: docker/login-action@v3
 with:
 registry: }
 username: }
 password: }

 - id: meta
 uses: docker/metadata-action@v5
 with:
 images: }/}
 tags: |
 type=sha, prefix=
 type=raw, value=latest

 - uses: docker/build-push-action@v5
 with:
 context: .
 push: true
 tags: }

 update-manifests:
 needs: build-and-push
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 with:
 repository: myorg/medusa-k8s
 token: }

 - name: Update image tag
 run: |
 SHORT_SHA=$(echo } | cut -c1-7)
 cd k8s/overlays/production
 kustomize edit set image myregistry/medusa-backend=}/}:

 - name: Commit and push
 run: |
 git config user.name "GitHub Actions"
 git config user.email "actions@github.com"
 git add .
 git commit -m "Deploy medusa-backend: }"
 git push

จัดการ Database Migration และ Seed Data

สร้าง migration workflow ที่ปลอดภัยสำหรับ production

#!/bin/bash
# scripts/migrate.sh — Safe Database Migration
set -euo pipefail

echo "=== Medusa Database Migration ==="
echo "Environment: "
echo "Database: @***"
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"

# 1. Backup ก่อน migrate
echo "[1/4] Creating database backup..."
BACKUP_FILE="/tmp/medusa_backup_$(date +%Y%m%d_%H%M%S).sql"
pg_dump "$DATABASE_URL" > "$BACKUP_FILE"
echo " Backup saved: $BACKUP_FILE ($(du -sh $BACKUP_FILE | cut -f1))"

# 2. รัน migration (dry-run ก่อน)
echo "[2/4] Checking pending migrations..."
npx medusa migrations run --dry-run 2>&1 | tee /tmp/migration_plan.txt
PENDING=$(grep -c "migration" /tmp/migration_plan.txt || true)
echo " Pending migrations: $PENDING"

if [ "$PENDING" -eq 0 ]; then
 echo "No pending migrations. Done."
 exit 0
fi

# 3. รัน migration จริง
echo "[3/4] Running migrations..."
npx medusa migrations run 2>&1 | tee /tmp/migration_result.txt

if [ $? -ne 0 ]; then
 echo "ERROR: Migration failed! Rolling back..."
 psql "$DATABASE_URL" < "$BACKUP_FILE"
 echo "Rollback complete."
 exit 1
fi

# 4. Verify
echo "[4/4] Verifying migration..."
npx medusa migrations run --dry-run 2>&1 | grep -c "migration" && echo " WARNING: Still pending!" || echo " All migrations applied."

echo "=== Migration Complete ==="

# Seed data script
cat > scripts/seed.js << 'SEED'
const { MedusaContainer } = require("@medusajs/medusa");

async function seed() {
 const container = await MedusaContainer.init();
 const productService = container.resolve("productService");
 const regionService = container.resolve("regionService");

 // สร้าง Region
 const region = await regionService.create({
 name: "Thailand",
 currency_code: "thb",
 tax_rate: 7,
 payment_providers: ["manual"],
 fulfillment_providers: ["manual"],
 countries: ["th"],
 });
 console.log(`Region created: `);

 // สร้าง Product
 const product = await productService.create({
 title: "Sample T-Shirt",
 description: "A comfortable cotton t-shirt",
 handle: "sample-tshirt",
 is_giftcard: false,
 options: [{ title: "Size" }],
 variants: [
 { title: "S", prices: [{ amount: 590, currency_code: "thb" }], options: [{ value: "S" }], inventory_quantity: 100 },
 { title: "M", prices: [{ amount: 590, currency_code: "thb" }], options: [{ value: "M" }], inventory_quantity: 100 },
 { title: "L", prices: [{ amount: 590, currency_code: "thb" }], options: [{ value: "L" }], inventory_quantity: 100 },
 ],
 });
 console.log(`Product created: `);
}

seed().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
SEED

Monitoring และ Rollback Strategy

ตั้งค่า monitoring และ automated rollback สำหรับ Medusa deployment

#!/usr/bin/env python3
# medusa_monitor.py — Medusa Commerce Health Monitor
import requests
import time
import json
from datetime import datetime

MEDUSA_URL = "http://localhost:9000"
ARGOCD_URL = "http://argocd-server.argocd.svc:443"
ARGOCD_TOKEN = "your-argocd-token"
SLACK_WEBHOOK = "https://hooks.slack.com/services/xxx"

def check_health():
 try:
 r = requests.get(f"{MEDUSA_URL}/health", timeout=10)
 return r.status_code == 200
 except Exception:
 return False

def check_api():
 endpoints = [
 "/store/products?limit=1",
 "/store/regions",
 "/store/collections",
 ]
 results = {}
 for ep in endpoints:
 try:
 r = requests.get(f"{MEDUSA_URL}{ep}", timeout=10)
 results[ep] = {
 "status": r.status_code,
 "latency": r.elapsed.total_seconds(),
 "ok": r.status_code == 200
 }
 except Exception as e:
 results[ep] = {"status": 0, "latency": 0, "ok": False, "error": str(e)}
 return results

def rollback_argocd(app_name="medusa-commerce"):
 headers = {"Authorization": f"Bearer {ARGOCD_TOKEN}"}
 
 # Get current sync history
 r = requests.get(
 f"{ARGOCD_URL}/api/v1/applications/{app_name}",
 headers=headers, verify=False
 )
 app = r.json()
 history = app.get("status", {}).get("history", [])
 
 if len(history) < 2:
 print("No previous revision to rollback to")
 return False
 
 prev_revision = history[-2]["revision"]
 print(f"Rolling back to revision: {prev_revision}")
 
 # Rollback
 r = requests.post(
 f"{ARGOCD_URL}/api/v1/applications/{app_name}/rollback",
 headers=headers,
 json={"id": history[-2]["id"]},
 verify=False
 )
 return r.status_code == 200

def notify(message, severity="info"):
 icon = {"critical": "🔴", "warning": "🟡", "info": "🟢"}[severity]
 print(f"[{icon}] {datetime.now()} {message}")
 if SLACK_WEBHOOK and severity != "info":
 requests.post(SLACK_WEBHOOK, json={"text": f"{icon} {message}"})

def monitor_loop():
 consecutive_failures = 0
 max_failures = 3
 
 print(f"Medusa Monitor started: {MEDUSA_URL}")
 
 while True:
 healthy = check_health()
 api_results = check_api()
 all_ok = all(r["ok"] for r in api_results.values())
 
 if healthy and all_ok:
 consecutive_failures = 0
 latencies = [r["latency"] for r in api_results.values() if r["ok"]]
 avg_lat = sum(latencies) / len(latencies) if latencies else 0
 
 if avg_lat > 2.0:
 notify(f"High latency: {avg_lat:.2f}s", "warning")
 else:
 consecutive_failures += 1
 failed = [ep for ep, r in api_results.items() if not r["ok"]]
 notify(f"Health check failed ({consecutive_failures}/{max_failures}): {failed}", "warning")
 
 if consecutive_failures >= max_failures:
 notify("Triggering automatic rollback!", "critical")
 if rollback_argocd():
 notify("Rollback initiated successfully", "warning")
 consecutive_failures = 0
 else:
 notify("Rollback failed! Manual intervention required", "critical")
 
 time.sleep(30)

if __name__ == "__main__":
 monitor_loop()

FAQ คำถามที่พบบ่อย

Q: Medusa กับ Shopify ต่างกันอย่างไร?

A: Medusa เป็น Open Source self-hosted ไม่มีค่าบริการรายเดือน ปรับแต่งได้ 100% เหมาะสำหรับธุรกิจที่ต้องการ full control ส่วน Shopify เป็น SaaS ใช้งานง่ายไม่ต้องจัดการ server แต่มีค่าบริการรายเดือนและ transaction fee ปรับแต่งได้จำกัด สำหรับ startup ที่มีทีม dev แนะนำ Medusa สำหรับธุรกิจที่ไม่มีทีม dev แนะนำ Shopify

Q: GitOps ดีกว่า CI/CD แบบปกติอย่างไร?

A: GitOps ใช้ Git เป็น single source of truth ทำให้ทุกการเปลี่ยนแปลงถูกบันทึกและ review ได้ สามารถ rollback ได้ด้วย git revert มี drift detection ที่ตรวจจับเมื่อ cluster state ไม่ตรงกับ Git ส่วน CI/CD แบบปกติอาจมี manual changes ที่ไม่ถูกบันทึก GitOps เหมาะสำหรับ Kubernetes environments

Q: Database migration กับ GitOps ทำอย่างไร?

A: ควรแยก migration ออกจาก application deployment โดยรัน migration เป็น Kubernetes Job หรือ init container ก่อนที่ application จะ start ต้อง backup database ก่อน migrate เสมอ ใช้ backward-compatible migrations ที่ทำงานได้กับทั้ง code เวอร์ชันเก่าและใหม่ ห้ามลบ column โดยตรงแต่ต้อง deprecate ก่อน

Q: ArgoCD กับ Flux CD ควรเลือกอันไหน?

A: ArgoCD มี Web UI ที่สวยและใช้งานง่าย มี application-centric approach เหมาะสำหรับทีมที่ต้องการ visibility ส่วน Flux CD เป็น lightweight มากกว่า ใช้ CRDs เป็นหลัก เหมาะสำหรับทีมที่ prefer CLI-first approach ทั้งสองตัวเป็น CNCF graduated projects สำหรับผู้เริ่มต้นแนะนำ ArgoCD เพราะ UI ช่วยให้เข้าใจง่าย