SiamCafe · Blog
BetterUptime กับ GitOps Workflow — วิธีใช้
บทความ

BetterUptime กับ GitOps Workflow — วิธีใช้

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

GitOps สำหรับ Monitoring

BetterUptime กับ GitOps Workflow — วิธีใช้

GitOps เป็นแนวทางที่ใช้ Git เป็น Single Source of Truth สำหรับทุกอย่างในระบบ รวมถึง Monitoring Configuration เมื่อรวม BetterUptime กับ GitOps ได้ระบบ Monitoring ที่จัดการผ่าน Code ทุกการเปลี่ยนแปลงผ่าน PR มี Review มี Audit Trail

ข้อดีคือ Version Control ย้อนกลับได้ Consistency ทุก Environment เหมือนกัน Automation ไม่ต้อง Manual Setup และ Review Process ป้องกันผิดพลาด

BetterUptime Configuration as Code

# === BetterUptime GitOps Configuration ===
# monitoring/config.yaml — Monitoring Configuration in Git

monitors:
  - name: "Production API"
    url: "https://api.example.com/health"
    monitor_type: "status"
    check_frequency: 30
    request_timeout: 15
    confirmation_period: 3
    regions: ["us", "eu", "as"]
    expected_status_codes: [200]
    alert_channels: ["slack-ops", "pagerduty-oncall"]

  - name: "Production Frontend"
    url: "https://www.example.com"
    monitor_type: "status"
    check_frequency: 60
    request_timeout: 30
    regions: ["us", "eu", "as", "au"]
    alert_channels: ["slack-ops"]

  - name: "Database Health"
    url: "https://api.example.com/db-health"
    monitor_type: "status"
    check_frequency: 60
    request_timeout: 10
    alert_channels: ["slack-ops", "pagerduty-oncall"]

  - name: "Staging API"
    url: "https://staging-api.example.com/health"
    monitor_type: "status"
    check_frequency: 120
    alert_channels: ["slack-dev"]

  - name: "SSL Certificate"
    url: "https://www.example.com"
    monitor_type: "ssl"
    check_frequency: 86400
    alert_channels: ["slack-ops"]

heartbeats:
  - name: "Backup Job"
    period: 86400
    grace: 3600
    alert_channels: ["slack-ops"]

  - name: "Data Sync"
    period: 3600
    grace: 600
    alert_channels: ["slack-ops"]

status_pages:
  - name: "Example Status"
    subdomain: "status-example"
    custom_domain: "status.example.com"
    sections:
      - name: "Core Services"
        monitors: ["Production API", "Production Frontend"]
      - name: "Infrastructure"
        monitors: ["Database Health"]

alert_channels:
  slack-ops:
    type: "slack"
    webhook: ""
  slack-dev:
    type: "slack"
    webhook: ""
  pagerduty-oncall:
    type: "pagerduty"
    key: ""

Python — GitOps Sync Script

BetterUptime กับ GitOps Workflow — วิธีใช้
# gitops_sync.py — Sync BetterUptime Config จาก Git
# pip install requests pyyaml

import yaml
import requests
import os
import json
import sys
from pathlib import Path

class BetterUptimeGitOps:
    """Sync BetterUptime Configuration จาก YAML Config"""

    BASE_URL = "https://betteruptime.com/api/v2"

    def __init__(self, api_token):
        self.session = requests.Session()
        self.session.headers = {
            "Authorization": f"Bearer {api_token}",
            "Content-Type": "application/json",
        }

    def load_config(self, config_path):
        """โหลด Config จาก YAML"""
        with open(config_path) as f:
            raw = f.read()

        # Replace environment variables
        for key, value in os.environ.items():
            raw = raw.replace(f"}}", value)

        return yaml.safe_load(raw)

    def get_existing_monitors(self):
        """ดึง Monitors ที่มีอยู่"""
        resp = self.session.get(f"{self.BASE_URL}/monitors")
        monitors = resp.json().get("data", [])
        return {m["attributes"]["pronounceable_name"]: m for m in monitors}

    def sync_monitors(self, config):
        """Sync Monitors ตาม Config"""
        existing = self.get_existing_monitors()
        desired = {m["name"]: m for m in config.get("monitors", [])}

        created, updated, deleted = 0, 0, 0

        # Create or Update
        for name, spec in desired.items():
            payload = {
                "pronounceable_name": name,
                "url": spec["url"],
                "monitor_type": spec.get("monitor_type", "status"),
                "check_frequency": spec.get("check_frequency", 60),
                "request_timeout": spec.get("request_timeout", 15),
                "confirmation_period": spec.get("confirmation_period", 3),
                "regions": ",".join(spec.get("regions", ["us", "eu"])),
            }

            if name in existing:
                # Update
                monitor_id = existing[name]["id"]
                resp = self.session.patch(
                    f"{self.BASE_URL}/monitors/{monitor_id}",
                    json=payload,
                )
                if resp.status_code == 200:
                    updated += 1
                    print(f"  Updated: {name}")
            else:
                # Create
                resp = self.session.post(
                    f"{self.BASE_URL}/monitors",
                    json=payload,
                )
                if resp.status_code in [200, 201]:
                    created += 1
                    print(f"  Created: {name}")

        # Delete monitors not in config
        for name, monitor in existing.items():
            if name not in desired:
                resp = self.session.delete(
                    f"{self.BASE_URL}/monitors/{monitor['id']}"
                )
                if resp.status_code in [200, 204]:
                    deleted += 1
                    print(f"  Deleted: {name}")

        return {"created": created, "updated": updated, "deleted": deleted}

    def sync_heartbeats(self, config):
        """Sync Heartbeats"""
        for hb in config.get("heartbeats", []):
            payload = {
                "name": hb["name"],
                "period": hb.get("period", 3600),
                "grace": hb.get("grace", 300),
            }
            resp = self.session.post(f"{self.BASE_URL}/heartbeats", json=payload)
            status = "created" if resp.status_code in [200, 201] else "exists"
            print(f"  Heartbeat {hb['name']}: {status}")

    def plan(self, config_path):
        """Plan — แสดงการเปลี่ยนแปลงที่จะเกิดขึ้น (Dry Run)"""
        config = self.load_config(config_path)
        existing = self.get_existing_monitors()
        desired = {m["name"]: m for m in config.get("monitors", [])}

        print("\n=== GitOps Plan ===")
        for name in desired:
            if name in existing:
                print(f"  ~ Update: {name}")
            else:
                print(f"  + Create: {name}")

        for name in existing:
            if name not in desired:
                print(f"  - Delete: {name}")

    def apply(self, config_path):
        """Apply — Sync Configuration"""
        config = self.load_config(config_path)

        print("\n=== GitOps Apply ===")
        print("\nMonitors:")
        result = self.sync_monitors(config)
        print(f"\n  Summary: {result['created']} created, "
              f"{result['updated']} updated, {result['deleted']} deleted")

        print("\nHeartbeats:")
        self.sync_heartbeats(config)

        print("\n=== Apply Complete ===")

# Usage:
# gitops = BetterUptimeGitOps(os.environ["BETTERUPTIME_TOKEN"])
# gitops.plan("monitoring/config.yaml")
# gitops.apply("monitoring/config.yaml")

CI/CD Pipeline สำหรับ GitOps Monitoring

# === GitHub Actions — GitOps Monitoring Pipeline ===
# .github/workflows/monitoring-gitops.yml

name: Monitoring GitOps
on:
  push:
    branches: [main]
    paths: ["monitoring/**"]
  pull_request:
    branches: [main]
    paths: ["monitoring/**"]

env:
  BETTERUPTIME_TOKEN: }
  SLACK_OPS_WEBHOOK: }
  SLACK_DEV_WEBHOOK: }
  PAGERDUTY_KEY: }

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Dependencies
        run: pip install pyyaml jsonschema

      - name: Validate Config
        run: |
          python -c "
          import yaml, sys
          with open('monitoring/config.yaml') as f:
              config = yaml.safe_load(f.read())
          monitors = config.get('monitors', [])
          print(f'Monitors: {len(monitors)}')
          for m in monitors:
              assert 'name' in m, f'Missing name in monitor'
              assert 'url' in m, f'Missing url in {m[\"name\"]}'
              print(f'  OK: {m[\"name\"]}')
          print('Validation passed!')
          "

  plan:
    runs-on: ubuntu-latest
    needs: validate
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install requests pyyaml

      - name: Plan Changes
        run: python scripts/gitops_sync.py plan monitoring/config.yaml

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            const output = `### Monitoring GitOps Plan
            Changes detected in monitoring configuration.
            Review the plan output above before approving.`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });

  apply:
    runs-on: ubuntu-latest
    needs: validate
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install requests pyyaml

      - name: Apply Changes
        run: python scripts/gitops_sync.py apply monitoring/config.yaml

      - name: Notify Slack
        if: success()
        run: |
          curl -X POST $SLACK_OPS_WEBHOOK \
            -H 'Content-Type: application/json' \
            -d '{"text":"Monitoring config updated via GitOps"}'

# === ArgoCD Application สำหรับ Monitoring Stack ===
# argocd/monitoring-app.yaml
# apiVersion: argoproj.io/v1alpha1
# kind: Application
# metadata:
#   name: monitoring-stack
#   namespace: argocd
# spec:
#   project: default
#   source:
#     repoURL: https://github.com/myorg/monitoring-config
#     path: k8s/monitoring
#     targetRevision: main
#   destination:
#     server: https://kubernetes.default.svc
#     namespace: monitoring
#   syncPolicy:
#     automated:
#       prune: true
#       selfHeal: true
#     syncOptions:
#       - CreateNamespace=true

Best Practices

  • Config in Git: เก็บ Monitoring Config ใน Git Repository เหมือน Infrastructure Code
  • PR Review: ทุกการเปลี่ยน Monitor ต้องผ่าน Pull Request มี Review ก่อน Apply
  • Plan Before Apply: แสดง Plan ก่อน Apply เหมือน Terraform Plan ป้องกันผิดพลาด
  • Environment Variables: เก็บ Secrets (API Keys, Webhooks) ใน CI/CD Secrets ไม่ Hardcode
  • Drift Detection: ตรวจสอบว่า Config จริงตรงกับ Git อย่างสม่ำเสมอ
  • Rollback: ถ้า Apply ผิดพลาด Revert Git Commit แล้ว Apply ใหม่

GitOps คืออะไร

แนวทางจัดการ Infrastructure โดยใช้ Git เป็น Single Source of Truth ทุกการเปลี่ยนแปลงผ่าน PR มี Review ใช้ ArgoCD Flux CD Reconcile สถานะจริงให้ตรงกับ Git อัตโนมัติ