Technology

CircleCI Orbs SaaS Architecture

circleci orbs saas architecture
CircleCI Orbs SaaS Architecture | SiamCafe Blog
2025-10-02· อ. บอม — SiamCafe.net· 8,670 คำ

CircleCI Orbs สำหรับ SaaS

SaaS Architecture มักประกอบด้วยหลาย Microservices, Database, Message Queue, Cache และ External Services การสร้าง CI/CD Pipeline ที่มีประสิทธิภาพต้องจัดการ Build, Test และ Deploy หลาย Service พร้อมกัน CircleCI Orbs ช่วยลดความซับซ้อนด้วย Reusable Configuration

บทความนี้แสดงวิธีสร้าง CI/CD Pipeline สำหรับ SaaS ที่ครอบคลุม Multi-service Build, Database Migration, Feature Flags, Environment Promotion และ Deployment Strategies ต่างๆ

Multi-service CI/CD Pipeline

# .circleci/config.yml — SaaS Multi-service Pipeline
version: 2.1

orbs:
  aws-ecr: circleci/aws-ecr@9.0.4
  aws-eks: circleci/aws-eks@2.2.0
  slack: circleci/slack@4.13.3
  node: circleci/node@5.2.0
  python: circleci/python@2.1.1

# Parameters สำหรับ Dynamic Configuration
parameters:
  run-api:
    type: boolean
    default: false
  run-web:
    type: boolean
    default: false
  run-worker:
    type: boolean
    default: false
  run-all:
    type: boolean
    default: false

# Executors
executors:
  node-executor:
    docker:
      - image: cimg/node:20.11
    resource_class: medium
  python-executor:
    docker:
      - image: cimg/python:3.12
      - image: cimg/postgres:16.1  # Test Database
        environment:
          POSTGRES_DB: test_db
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
      - image: cimg/redis:7.2      # Test Redis
    resource_class: medium

# Reusable Commands
commands:
  setup-env:
    parameters:
      service:
        type: string
    steps:
      - checkout
      - run:
          name: Setup Environment
          command: |
            echo "SERVICE=<< parameters.service >>" >> $BASH_ENV
            echo "IMAGE_TAG=" >> $BASH_ENV
            echo "REGISTRY=.dkr.ecr..amazonaws.com" >> $BASH_ENV

  run-migrations:
    parameters:
      environment:
        type: string
    steps:
      - run:
          name: Run Database Migrations
          command: |
            cd services/api
            # Dry-run migration ก่อน
            python manage.py migrate --check
            # รัน Migration จริง
            python manage.py migrate --no-input
          environment:
            DATABASE_URL: << parameters.environment >>

  notify-slack:
    parameters:
      status:
        type: string
      service:
        type: string
    steps:
      - slack/notify:
          channel: deployments
          event: << parameters.status >>
          template: |
            {
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "<< parameters.status >> | *<< parameters.service >>* deployed to \nCommit:  by "
                  }
                }
              ]
            }

# Jobs
jobs:
  # Path Filtering — ตรวจสอบว่า Service ไหนเปลี่ยน
  detect-changes:
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - run:
          name: Detect Changed Services
          command: |
            # เปรียบเทียบกับ main branch
            CHANGED=$(git diff --name-only origin/main...HEAD)
            echo "Changed files:"
            echo "$CHANGED"

            API_CHANGED=$(echo "$CHANGED" | grep -c "services/api/" || true)
            WEB_CHANGED=$(echo "$CHANGED" | grep -c "services/web/" || true)
            WORKER_CHANGED=$(echo "$CHANGED" | grep -c "services/worker/" || true)
            SHARED_CHANGED=$(echo "$CHANGED" | grep -c "shared/" || true)

            # ถ้า Shared เปลี่ยน Build ทุก Service
            if [ "$SHARED_CHANGED" -gt 0 ]; then
              echo '{"run-all": true}' > /tmp/pipeline-params.json
            else
              echo "{\"run-api\": $([ $API_CHANGED -gt 0 ] && echo true || echo false), \"run-web\": $([ $WEB_CHANGED -gt 0 ] && echo true || echo false), \"run-worker\": $([ $WORKER_CHANGED -gt 0 ] && echo true || echo false)}" > /tmp/pipeline-params.json
            fi

            cat /tmp/pipeline-params.json
      - persist_to_workspace:
          root: /tmp
          paths: [pipeline-params.json]

  # Build & Test API Service
  build-api:
    executor: python-executor
    steps:
      - setup-env:
          service: api
      - python/install-packages:
          pkg-manager: pip
          app-dir: services/api
      - run:
          name: Run Linting
          command: |
            cd services/api
            ruff check .
            mypy .
      - run:
          name: Run Tests
          command: |
            cd services/api
            pytest --cov=. --cov-report=xml --junitxml=test-results/results.xml -v
      - store_test_results:
          path: services/api/test-results
      - store_artifacts:
          path: services/api/coverage.xml

  # Build & Test Web Service
  build-web:
    executor: node-executor
    steps:
      - setup-env:
          service: web
      - node/install-packages:
          app-dir: services/web
      - run:
          name: Lint & Type Check
          command: |
            cd services/web
            npm run lint
            npm run type-check
      - run:
          name: Run Tests
          command: |
            cd services/web
            npm run test -- --coverage --ci
      - run:
          name: Build
          command: |
            cd services/web
            npm run build
      - persist_to_workspace:
          root: .
          paths: [services/web/dist]

  # Docker Build & Push
  docker-build:
    parameters:
      service:
        type: string
    docker:
      - image: cimg/base:current
    steps:
      - setup-env:
          service: << parameters.service >>
      - setup_remote_docker:
          docker_layer_caching: true
      - run:
          name: Build & Push Docker Image
          command: |
            aws ecr get-login-password | docker login --username AWS --password-stdin $REGISTRY
            docker build -t $REGISTRY/<< parameters.service >>:$IMAGE_TAG \
              -f services/<< parameters.service >>/Dockerfile \
              --build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
              --build-arg VCS_REF=$CIRCLE_SHA1 \
              .
            docker push $REGISTRY/<< parameters.service >>:$IMAGE_TAG

  # Deploy to Environment
  deploy:
    parameters:
      service:
        type: string
      environment:
        type: string
    docker:
      - image: cimg/base:current
    steps:
      - checkout
      - run:
          name: Deploy << parameters.service >> to << parameters.environment >>
          command: |
            # Update Kubernetes Deployment
            kubectl set image deployment/<< parameters.service >> \
              << parameters.service >>=$REGISTRY/<< parameters.service >>:$IMAGE_TAG \
              -n << parameters.environment >>

            # Wait for Rollout
            kubectl rollout status deployment/<< parameters.service >> \
              -n << parameters.environment >> --timeout=300s
      - notify-slack:
          status: pass
          service: << parameters.service >>

# Workflows
workflows:
  build-test-deploy:
    jobs:
      - detect-changes:
          filters:
            branches:
              ignore: main

      - build-api:
          requires: [detect-changes]
          filters:
            branches:
              ignore: main

      - build-web:
          requires: [detect-changes]

      - docker-build:
          name: docker-api
          service: api
          requires: [build-api]
          context: aws-prod

      - docker-build:
          name: docker-web
          service: web
          requires: [build-web]
          context: aws-prod

      - deploy:
          name: deploy-staging-api
          service: api
          environment: staging
          requires: [docker-api]
          context: aws-staging

      - deploy:
          name: deploy-staging-web
          service: web
          environment: staging
          requires: [docker-web]
          context: aws-staging

      # Production Deploy — ต้อง Approve
      - hold-production:
          type: approval
          requires:
            - deploy-staging-api
            - deploy-staging-web

      - deploy:
          name: deploy-prod-api
          service: api
          environment: production
          requires: [hold-production]
          context: aws-prod

      - deploy:
          name: deploy-prod-web
          service: web
          environment: production
          requires: [hold-production]
          context: aws-prod

Database Migration Pipeline

# migration-check.py — ตรวจสอบ Migration ก่อน Deploy
import subprocess
import sys
import json

def check_migration_safety(migration_file):
    """ตรวจสอบว่า Migration ปลอดภัยสำหรับ Zero-downtime"""
    dangerous_ops = [
        "DROP TABLE",
        "DROP COLUMN",
        "ALTER COLUMN",   # เปลี่ยน Type อาจ Lock Table
        "RENAME TABLE",
        "RENAME COLUMN",
        "NOT NULL",       # เพิ่ม NOT NULL constraint อาจ Fail
    ]

    with open(migration_file, "r") as f:
        content = f.read().upper()

    issues = []
    for op in dangerous_ops:
        if op in content:
            issues.append(f"Dangerous operation found: {op}")

    if issues:
        print(f"MIGRATION SAFETY CHECK FAILED: {migration_file}")
        for issue in issues:
            print(f"  - {issue}")
        print("\nUse Expand-Contract pattern:")
        print("  Phase 1 (Expand): Add new columns, keep old ones")
        print("  Phase 2 (Deploy): Update code to use both")
        print("  Phase 3 (Contract): Remove old columns")
        return False

    print(f"MIGRATION SAFE: {migration_file}")
    return True

# ตรวจสอบ Migration ใน CI
def ci_check():
    result = subprocess.run(
        ["python", "manage.py", "showmigrations", "--plan", "--format", "json"],
        capture_output=True, text=True,
    )
    pending = json.loads(result.stdout)
    all_safe = True

    for migration in pending:
        if not migration.get("applied"):
            sql = subprocess.run(
                ["python", "manage.py", "sqlmigrate",
                 migration["app"], migration["name"]],
                capture_output=True, text=True,
            )
            # เขียน SQL ไปไฟล์ชั่วคราว
            tmp = f"/tmp/{migration['name']}.sql"
            with open(tmp, "w") as f:
                f.write(sql.stdout)

            if not check_migration_safety(tmp):
                all_safe = False

    if not all_safe:
        sys.exit(1)
    print("All migrations are safe for zero-downtime deployment")

ci_check()

Feature Flags Integration

# feature_flags.py — Feature Flags สำหรับ SaaS Deployment
import os
import json
import hashlib
from datetime import datetime

class FeatureFlagManager:
    """จัดการ Feature Flags สำหรับ Gradual Rollout"""

    def __init__(self, config_path="feature_flags.json"):
        with open(config_path) as f:
            self.flags = json.load(f)

    def is_enabled(self, flag_name, user_id=None, tenant_id=None):
        """ตรวจสอบว่า Feature Flag เปิดหรือไม่"""
        flag = self.flags.get(flag_name)
        if not flag:
            return False

        # Global kill switch
        if not flag.get("enabled", False):
            return False

        # Check rollout percentage
        percentage = flag.get("rollout_percentage", 100)
        if percentage < 100 and user_id:
            # Consistent hashing — User เดิมได้ผลเดิมเสมอ
            hash_input = f"{flag_name}:{user_id}"
            hash_val = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
            if (hash_val % 100) >= percentage:
                return False

        # Check tenant allowlist
        allowed_tenants = flag.get("allowed_tenants", [])
        if allowed_tenants and tenant_id:
            if tenant_id not in allowed_tenants:
                return False

        # Check date range
        start = flag.get("start_date")
        end = flag.get("end_date")
        now = datetime.now().isoformat()
        if start and now < start:
            return False
        if end and now > end:
            return False

        return True

    def get_all_flags(self, user_id=None, tenant_id=None):
        """ดึง Flag ทั้งหมดพร้อมสถานะ"""
        result = {}
        for name in self.flags:
            result[name] = self.is_enabled(name, user_id, tenant_id)
        return result

# feature_flags.json
# {
#   "new_dashboard": {
#     "enabled": true,
#     "rollout_percentage": 25,
#     "allowed_tenants": ["tenant_001", "tenant_002"],
#     "description": "New dashboard UI"
#   },
#   "ai_assistant": {
#     "enabled": true,
#     "rollout_percentage": 10,
#     "start_date": "2025-01-01",
#     "description": "AI-powered assistant feature"
#   }
# }

# ใช้ใน CI/CD — Deploy แล้วเปิด Feature Flag ทีละ %
# Step 1: Deploy code (Feature Flag off)
# Step 2: Enable 5% → Monitor
# Step 3: Enable 25% → Monitor
# Step 4: Enable 100% → Stable

Deployment Strategies สำหรับ SaaS

CircleCI Orbs คืออะไร

CircleCI Orbs เป็น Reusable Configuration Packages รวม Jobs, Commands และ Executors ลดการเขียน Config ซ้ำ มี Orbs สำเร็จรูปสำหรับ AWS, Docker, Kubernetes, Slack ใช้แค่ Reference แล้ว Config ไม่กี่บรรทัด ช่วยให้สร้าง Pipeline ได้เร็ว

SaaS CI/CD ต่างจาก CI/CD ทั่วไปอย่างไร

SaaS CI/CD จัดการหลาย Service พร้อมกัน มี Database Migration แบบ Zero-downtime, Feature Flags, Multi-tenant Testing, Environment Promotion และ Canary/Blue-Green Deployment ซับซ้อนกว่า Single App CI/CD มาก

วิธีจัดการ Multi-service Build ใน CircleCI ทำอย่างไร

ใช้ Dynamic Configuration กับ Path Filtering ตรวจสอบว่า Service ไหนเปลี่ยนแล้ว Build เฉพาะ Service นั้น ใช้ Matrix Jobs สำหรับ Build หลาย Service พร้อมกัน Workspace ส่ง Artifacts ระหว่าง Jobs ประหยัด Build Minutes

Database Migration แบบ Zero-downtime ทำอย่างไร

ใช้ Expand-Contract Pattern แยก Migration เป็น 2 Phase Expand เพิ่ม Column ใหม่ Deploy Code ที่ใช้ทั้งเก่าและใหม่ Contract ลบ Column เก่า ไม่ทำ Destructive Changes ใน Migration เดียว ตรวจสอบ Migration Safety ใน CI

สรุป

CircleCI Orbs ช่วยให้สร้าง CI/CD Pipeline สำหรับ SaaS ได้อย่างมีประสิทธิภาพ ด้วย Multi-service Build ที่ Build เฉพาะ Service ที่เปลี่ยน, Database Migration ที่ตรวจสอบความปลอดภัยอัตโนมัติ, Feature Flags สำหรับ Gradual Rollout และ Deployment Strategies เช่น Canary, Blue-Green สิ่งสำคัญคือ Automate ทุกอย่าง ตรวจสอบ Migration Safety ใน CI และ Monitor หลัง Deploy

📖 บทความที่เกี่ยวข้อง

CircleCI Orbs Team Productivityอ่านบทความ → Radix UI Primitives SaaS Architectureอ่านบทความ → CircleCI Orbs Performance Tuning เพิ่มความเร็วอ่านบทความ → CircleCI Orbs DNS Managementอ่านบทความ →

📚 ดูบทความทั้งหมด →