Module 32 NoOps 5 labs

NoOps, Auto-remediation và Self-healing

NoOps không phải "không cần vận hành" mà là tự động hóa vận hành tối đa: hệ thống tự phát hiện, tự khôi phục, tự scale — con người chỉ can thiệp khi vượt ngưỡng tự động. Module này bao gồm Kubernetes self-healing, HPA/Cluster Autoscaler, Prometheus alert → webhook → auto-remediation, KEDA event-driven scaling và auto rollback.

Công cụ thực hành kubectl, Helm, Prometheus, Alertmanager, KEDA, bash/Python webhook
Nền tảng Kubernetes (minikube/kind/k3s), Linux (WSL2), GitHub Actions
Thời điểm phát hành 23/05/2026
Ngày biên soạn 23/05/2026
Người biên soạn Trần Văn Hòa — Microsoft Certified Trainer (MCT)

Mục tiêu học tập

1. Lý thuyết cốt lõi

1.1. NoOps — khái niệm và hiểu đúng

NoOps (No Operations) là trạng thái lý tưởng nơi developer deploy và vận hành hoàn toàn tự phục vụ, không cần tương tác với Ops team. Đây là điểm cực của Platform Engineering: platform đủ tốt để ẩn đi toàn bộ complexity hạ tầng. Thực tế, NoOps không có nghĩa không cần kỹ sư vận hành — mà cần ít hơn, nhưng giỏi hơn để xây và duy trì platform tự động đó.

Phổ tự động hóa vận hành

  • Manual Ops — kỹ sư nhận alert, SSH vào máy, chạy lệnh thủ công.
  • Runbook Automation — alert kích hoạt runbook tự động, kỹ sư approve bước nguy hiểm.
  • Auto-remediation — hệ thống tự xử lý các sự cố đã biết (restart, scale, rollback) không cần người.
  • Self-healing — platform phát hiện và sửa trạng thái lệch khỏi desired state (Kubernetes reconcile loop).
  • NoOps — toàn bộ operational concerns ẩn sau platform abstraction; developer chỉ push code.

1.2. Kubernetes Self-healing — cơ chế nền tảng

Kubernetes self-healing dựa trên control loop (reconcile loop): controller liên tục so sánh desired state (spec) với actual state (status) và thực hiện hành động để hội tụ. Các cơ chế cụ thể:

1.3. Horizontal Pod Autoscaler (HPA) và Cluster Autoscaler

HPA scale số lượng pod dựa trên metrics (CPU, memory, custom metrics từ Prometheus). Formula tính toán:

desiredReplicas = ceil(currentReplicas × (currentMetric / targetMetric))

Cluster Autoscaler (CA) scale số lượng node: thêm node khi pod pending do thiếu resources, xóa node idle sau thời gian --scale-down-unneeded-time. CA tích hợp với cloud provider (AWS ASG, Azure VMSS, GCE MIG) để tạo/xóa VM thật.

1.4. Auto-remediation Pipeline

Pattern phổ biến: Prometheus → Alertmanager → Webhook → Remediation Script/Runbook. Alertmanager nhận alert firing, route đến webhook receiver (HTTP endpoint). Webhook server (Python Flask/Go/bash via nc) phân tích alert label và chạy hành động tương ứng: restart deployment, scale up, rollback, hoặc gọi API cloud provider. Với sự cố phức tạp hơn, webhook gọi vào automation platform (Ansible AWX, Rundeck) để chạy playbook.

1.5. KEDA — Kubernetes Event-Driven Autoscaling

KEDA (CNCF Graduated) mở rộng HPA với 60+ scalers cho các event source: RabbitMQ, Kafka, Azure Service Bus, SQS, Redis, Prometheus, HTTP traffic, cron schedule. KEDA có thể scale deployment về 0 replicas khi không có event (cost saving), và scale từ 0 lên ngay khi event xuất hiện — điều HPA thuần không làm được.

Cơ chếKích hoạt bởiScale về 0?Use case
HPACPU/Memory/Custom metricKhông (min=1)Web API, stateless service
KEDAEvent source (queue, topic...)Có (scale-to-zero)Consumer, batch job, cron
VPAHistorical resource usageKhôngTối ưu request/limit
CAPod pending (node thiếu)Node levelMulti-node cluster

2. Thực hành (Labs)

LAB-156

Prometheus Alert gọi Remediation Script qua Webhook

Prometheus · Alertmanager · Python · kubectl

🎯 Mục tiêu: Xây dựng pipeline hoàn chỉnh: Prometheus phát hiện pod error rate cao → Alertmanager gửi webhook → Python server nhận alert → tự động restart deployment.

🧰 Công cụ / nền tảng: minikube hoặc kind, kubectl, Helm, Python 3.8+, Prometheus stack (kube-prometheus-stack).

📦 Chuẩn bị:

# Cài minikube nếu chưa có
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
minikube start --memory=4096 --cpus=2

# Cài Prometheus stack qua Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-prom prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace \
  --set alertmanager.enabled=true \
  --set prometheus.prometheusSpec.retention=2h

# Xác nhận các pod đang running
kubectl -n monitoring get pods | grep -E 'prometheus|alertmanager'

▶️ Các bước:

# BƯỚC 1: Tạo webhook server (Python Flask)
mkdir -p auto-remediation && cd auto-remediation

cat > webhook_server.py << 'PYEOF'
#!/usr/bin/env python3
"""
Auto-remediation webhook server.
Nhận alert từ Alertmanager, thực hiện hành động tự động.
"""
from flask import Flask, request, jsonify
import subprocess
import logging
import json

app = Flask(__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')

ALLOWED_ACTIONS = {
    "HighErrorRate":    {"action": "rollout_restart", "namespace": "default"},
    "PodCrashLooping":  {"action": "rollout_restart", "namespace": "default"},
    "HighMemoryUsage":  {"action": "scale_up",        "namespace": "default"},
}

def rollout_restart(namespace: str, deployment: str) -> dict:
    cmd = ["kubectl", "rollout", "restart", f"deployment/{deployment}", "-n", namespace]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    return {"returncode": result.returncode, "stdout": result.stdout, "stderr": result.stderr}

def scale_up(namespace: str, deployment: str, replicas: int = 3) -> dict:
    cmd = ["kubectl", "scale", f"deployment/{deployment}", f"--replicas={replicas}", "-n", namespace]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    return {"returncode": result.returncode, "stdout": result.stdout}

@app.route('/webhook', methods=['POST'])
def handle_alert():
    data = request.get_json(force=True)
    alerts = data.get('alerts', [])
    results = []

    for alert in alerts:
        name = alert.get('labels', {}).get('alertname', '')
        deployment = alert.get('labels', {}).get('deployment', 'unknown')
        status = alert.get('status', '')  # firing | resolved

        app.logger.info(f"Received alert: {name} status={status} deployment={deployment}")

        if status != 'firing':
            results.append({"alert": name, "skipped": "not firing"})
            continue

        action_cfg = ALLOWED_ACTIONS.get(name)
        if not action_cfg:
            app.logger.warning(f"No action configured for alert: {name}")
            results.append({"alert": name, "skipped": "no action configured"})
            continue

        if action_cfg["action"] == "rollout_restart":
            result = rollout_restart(action_cfg["namespace"], deployment)
        elif action_cfg["action"] == "scale_up":
            result = scale_up(action_cfg["namespace"], deployment)
        else:
            result = {"error": "unknown action"}

        app.logger.info(f"Action result: {json.dumps(result)}")
        results.append({"alert": name, "action": action_cfg["action"], "result": result})

    return jsonify({"processed": len(results), "results": results}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001)
PYEOF

pip install flask
python webhook_server.py &   # chạy nền trên port 5001
# BƯỚC 2: Expose webhook agar Alertmanager trong cluster có thể gọi
# Lấy IP của host machine trong minikube network
WEBHOOK_HOST=$(minikube ssh "route -n | awk '/^0.0.0.0/{print \$2}'" 2>/dev/null || echo "192.168.49.1")
echo "Webhook host IP: $WEBHOOK_HOST"
# BƯỚC 3: Cấu hình Alertmanager gửi đến webhook
cat > alertmanager-config.yaml << EOF
apiVersion: monitoring.coreos.com/v1alpha1
kind: AlertmanagerConfig
metadata:
  name: auto-remediation
  namespace: monitoring
spec:
  route:
    groupBy: ['alertname', 'deployment']
    groupWait: 10s
    groupInterval: 30s
    repeatInterval: 1h
    receiver: 'auto-remediation-webhook'
    matchers:
      - name: severity
        value: critical
  receivers:
    - name: 'auto-remediation-webhook'
      webhookConfigs:
        - url: 'http://${WEBHOOK_HOST}:5001/webhook'
          sendResolved: true
          httpConfig:
            followRedirects: true
EOF
kubectl apply -f alertmanager-config.yaml
# BƯỚC 4: Tạo PrometheusRule để fire alert test
cat > test-alert-rule.yaml << 'EOF'
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: auto-remediation-rules
  namespace: monitoring
  labels:
    release: kube-prom   # label phải match với prometheus selector
spec:
  groups:
    - name: auto-remediation
      interval: 15s
      rules:
        - alert: HighErrorRate
          # Trigger thủ công bằng cách tạo metric giả
          expr: vector(1)   # luôn firing — chỉ dùng để test
          for: 15s
          labels:
            severity: critical
            deployment: test-app
          annotations:
            summary: "High error rate on {{ $labels.deployment }}"
            description: "Error rate > 5% for 1 minute"
EOF
kubectl apply -f test-alert-rule.yaml
# BƯỚC 5: Tạo deployment test để webhook có thứ để restart
kubectl create deployment test-app --image=nginx:alpine --replicas=2

# BƯỚC 6: Quan sát luồng
# Xem alert trong Prometheus UI:
kubectl -n monitoring port-forward svc/kube-prom-kube-prometheus-prometheus 9090 &
# Mở http://localhost:9090/alerts

# Xem Alertmanager:
kubectl -n monitoring port-forward svc/kube-prom-kube-prometheus-alertmanager 9093 &
# Mở http://localhost:9093

# Xem log webhook server:
# (terminal chạy webhook_server.py sẽ in log khi nhận alert)

✅ Kết quả mong đợi:

# Log webhook server in ra:
# 2026-05-23 10:xx:xx INFO Received alert: HighErrorRate status=firing deployment=test-app
# 2026-05-23 10:xx:xx INFO Action result: {"returncode": 0, "stdout": "deployment.apps/test-app restarted\n"}

# Xác nhận deployment đã được restart:
kubectl rollout history deployment/test-app
# REVISION  CHANGE-CAUSE
# 1         
# 2            ← restart mới từ auto-remediation

🧹 Cleanup:

kubectl delete -f test-alert-rule.yaml
kubectl delete -f alertmanager-config.yaml
kubectl delete deployment test-app
kill $(lsof -t -i:5001)   # dừng webhook server
LAB-157

Kubernetes Self-healing — Liveness Probe và Restart Policy

kubectl · Kubernetes · YAML

🎯 Mục tiêu: Quan sát K8s tự restart pod khi liveness probe thất bại, hiểu backoff behavior và phân biệt liveness vs readiness probe.

🧰 Công cụ / nền tảng: kubectl, minikube/kind.

📦 Chuẩn bị: Cluster K8s đang chạy từ LAB-156.

▶️ Các bước:

# BƯỚC 1: Deploy app với liveness probe HTTP
cat > self-healing-demo.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: self-healing-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: self-healing-app
  template:
    metadata:
      labels:
        app: self-healing-app
    spec:
      containers:
        - name: app
          image: nginx:alpine
          ports:
            - containerPort: 80
          # Readiness: K8s KHÔNG gửi traffic nếu fail
          readinessProbe:
            httpGet:
              path: /healthz
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3
          # Liveness: K8s RESTART container nếu fail
          livenessProbe:
            httpGet:
              path: /healthz
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 10
            failureThreshold: 3    # 3 lần fail liên tiếp → restart
            timeoutSeconds: 2
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 100m
              memory: 128Mi
EOF
kubectl apply -f self-healing-demo.yaml
# BƯỚC 2: Thêm /healthz endpoint vào nginx config
# nginx:alpine không có /healthz mặc định → ta sẽ tạo
kubectl exec -it deployment/self-healing-app -- sh -c \
  'echo "healthy" > /usr/share/nginx/html/healthz'

# Xác nhận probe pass:
kubectl get pods -l app=self-healing-app
# STATUS phải là Running, READY phải là 1/1
# BƯỚC 3: Mô phỏng liveness probe fail (xóa file healthz)
POD_NAME=$(kubectl get pods -l app=self-healing-app -o jsonpath='{.items[0].metadata.name}')
echo "Testing pod: $POD_NAME"

# Xóa file để probe trả về 404 → liveness fail
kubectl exec $POD_NAME -- rm /usr/share/nginx/html/healthz

# BƯỚC 4: Theo dõi K8s tự restart
# Mở terminal khác và watch:
kubectl get pods -l app=self-healing-app -w
# Sau ~30s (3 × 10s period) sẽ thấy:
# NAME                              READY   STATUS    RESTARTS   AGE
# self-healing-app-xxx-yyy          1/1     Running   0          2m
# self-healing-app-xxx-yyy          0/1     Running   1          2m   ← restart!
# self-healing-app-xxx-yyy          1/1     Running   1          2m   ← healed
# BƯỚC 5: Xem events để hiểu gì đã xảy ra
kubectl describe pod $POD_NAME | grep -A 20 "Events:"
# Events sẽ cho thấy:
#   Liveness probe failed: Get "http://...:80/healthz": 404 Not Found
#   Container app failed liveness probe, will be restarted
#   Container app started

# BƯỚC 6: Kiểm tra restart count và uptime
kubectl get pods -l app=self-healing-app
# RESTARTS column tăng lên; pod vẫn RUNNING → self-healing thành công

✅ Kết quả mong đợi: RESTARTS tăng từ 0 lên 1+ sau khi xóa healthz file. Pod tự trở về Running sau restart. kubectl describe ghi rõ lý do restart. Pod thứ 2 (không bị xóa file) vẫn RESTARTS=0 — K8s chỉ restart pod có vấn đề, không toàn bộ deployment.

🧹 Cleanup:

kubectl delete -f self-healing-demo.yaml
LAB-158

Auto Rollback khi Health Check Fail sau Deploy

kubectl · Deployment Strategy · bash

🎯 Mục tiêu: Xây dựng script deploy với auto-rollback: deploy image mới, monitor rollout status, tự rollback nếu health check không pass trong timeout.

🧰 Công cụ / nền tảng: kubectl, bash, minikube.

📦 Chuẩn bị: Deployment self-healing-app từ LAB-157 (hoặc tạo mới).

▶️ Các bước:

# BƯỚC 1: Tạo script deploy với auto-rollback
cat > deploy-with-rollback.sh << 'SHEOF'
#!/usr/bin/env bash
# deploy-with-rollback.sh
# Usage: ./deploy-with-rollback.sh   [timeout-seconds]
set -euo pipefail

DEPLOYMENT="${1:?Usage: $0   [timeout]}"
NEW_IMAGE="${2:?Provide new image}"
TIMEOUT="${3:-120}"   # default 120 giây
NAMESPACE="${NAMESPACE:-default}"

echo "=== Deploy: $DEPLOYMENT → $NEW_IMAGE (timeout=${TIMEOUT}s) ==="

# Lưu revision hiện tại để rollback
CURRENT_REVISION=$(kubectl rollout history deployment/"$DEPLOYMENT" -n "$NAMESPACE" \
  --no-headers | tail -1 | awk '{print $1}')
echo "Current revision: $CURRENT_REVISION"

# Tiến hành deploy
kubectl set image deployment/"$DEPLOYMENT" \
  "${DEPLOYMENT}=${NEW_IMAGE}" -n "$NAMESPACE"
echo "Image updated, monitoring rollout..."

# Monitor rollout với timeout
if kubectl rollout status deployment/"$DEPLOYMENT" \
     -n "$NAMESPACE" --timeout="${TIMEOUT}s"; then
  echo "✅ Rollout SUCCESS: $DEPLOYMENT is healthy"
  exit 0
else
  echo "❌ Rollout FAILED — initiating auto-rollback to revision $CURRENT_REVISION"
  kubectl rollout undo deployment/"$DEPLOYMENT" \
    -n "$NAMESPACE" --to-revision="$CURRENT_REVISION"
  # Chờ rollback hoàn thành
  kubectl rollout status deployment/"$DEPLOYMENT" -n "$NAMESPACE" --timeout=60s
  echo "⚠️  Rollback COMPLETE. Alert on-call: deployment $DEPLOYMENT reverted."
  exit 1
fi
SHEOF
chmod +x deploy-with-rollback.sh
# BƯỚC 2: Tạo deployment test (nginx ổn định)
kubectl create deployment rollback-demo --image=nginx:1.24-alpine --replicas=2
# Chờ pods ready
kubectl rollout status deployment/rollback-demo --timeout=60s
# BƯỚC 3: Test deploy với image KHÔNG TỒN TẠI → rollback sẽ trigger
# Image "nginx:THIS_TAG_DOES_NOT_EXIST" sẽ fail ImagePullBackOff
./deploy-with-rollback.sh rollback-demo nginx:THIS_TAG_DOES_NOT_EXIST 30

# Expected output:
# === Deploy: rollback-demo → nginx:THIS_TAG_DOES_NOT_EXIST (timeout=30s) ===
# Current revision: 1
# Image updated, monitoring rollout...
# error: deployment "rollback-demo" exceeded its progress deadline
# ❌ Rollout FAILED — initiating auto-rollback to revision 1
# Waiting for deployment "rollback-demo" rollout to finish: 0 out of 2...
# deployment "rollback-demo" successfully rolled out
# ⚠️  Rollback COMPLETE. Alert on-call: deployment rollback-demo reverted.
# BƯỚC 4: Verify rollback thành công
kubectl get pods -l app=rollback-demo
# Tất cả pods phải Running với image nginx:1.24-alpine (revision cũ)

kubectl rollout history deployment/rollback-demo
# Sẽ thấy revision 1 (original) và revision 3 (rollback undo về 1)

# BƯỚC 5: Test deploy thành công với image hợp lệ
./deploy-with-rollback.sh rollback-demo nginx:1.25-alpine 120
# ✅ Rollout SUCCESS: rollback-demo is healthy

✅ Kết quả mong đợi: Script tự rollback khi deploy image lỗi, không cần can thiệp thủ công. Exit code 1 khi rollback (CI/CD có thể bắt để notify). Deploy image hợp lệ thành công với exit code 0.

🧹 Cleanup:

kubectl delete deployment rollback-demo
rm deploy-with-rollback.sh
LAB-159

KEDA — Event-driven Autoscaling với RabbitMQ và Scale-to-Zero

KEDA · RabbitMQ · Helm · kubectl

🎯 Mục tiêu: Cài KEDA, deploy RabbitMQ, tạo consumer deployment scale từ 0 lên theo số message trong queue — và scale về 0 khi queue rỗng.

🧰 Công cụ / nền tảng: minikube, Helm, kubectl, KEDA v2, RabbitMQ.

📦 Chuẩn bị:

# Cài KEDA qua Helm
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
kubectl -n keda get pods   # keda-operator phải Running

# Cài RabbitMQ
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install rabbitmq bitnami/rabbitmq \
  --namespace default \
  --set auth.username=user \
  --set auth.password=password \
  --set replicaCount=1
kubectl rollout status statefulset/rabbitmq --timeout=120s

▶️ Các bước:

# BƯỚC 1: Tạo Secret cho KEDA kết nối RabbitMQ
kubectl create secret generic rabbitmq-secret \
  --from-literal=host="amqp://user:[email protected]:5672"
# BƯỚC 2: Deploy consumer (dùng image đơn giản, print message rồi sleep)
cat > keda-consumer.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: message-consumer
  namespace: default
spec:
  replicas: 0   # bắt đầu từ 0 — KEDA sẽ scale up
  selector:
    matchLabels:
      app: message-consumer
  template:
    metadata:
      labels:
        app: message-consumer
    spec:
      containers:
        - name: consumer
          image: busybox:latest
          command:
            - sh
            - -c
            - |
              echo "Consumer started, processing messages..."
              while true; do sleep 5; echo "Processing..."; done
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
EOF
kubectl apply -f keda-consumer.yaml
kubectl get pods -l app=message-consumer
# Pods: 0 — scale-to-zero đang hoạt động
# BƯỚC 3: Tạo KEDA ScaledObject
cat > keda-scaledobject.yaml << 'EOF'
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: rabbitmq-scaledobject
  namespace: default
spec:
  scaleTargetRef:
    name: message-consumer
  pollingInterval: 10     # kiểm tra mỗi 10 giây
  cooldownPeriod: 30      # chờ 30s trước khi scale down
  minReplicaCount: 0      # cho phép scale về 0
  maxReplicaCount: 10     # tối đa 10 pods
  triggers:
    - type: rabbitmq
      metadata:
        protocol: amqp
        queueName: task-queue
        mode: QueueLength    # scale dựa trên số message trong queue
        value: "5"           # 1 pod cho mỗi 5 messages
      authenticationRef:
        name: rabbitmq-trigger-auth
---
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: rabbitmq-trigger-auth
  namespace: default
spec:
  secretTargetRef:
    - parameter: host
      name: rabbitmq-secret
      key: host
EOF
kubectl apply -f keda-scaledobject.yaml

# Kiểm tra KEDA đã nhận ScaledObject
kubectl get scaledobject rabbitmq-scaledobject
# READY phải là True
# BƯỚC 4: Publish messages để trigger scale-up
# Port-forward RabbitMQ management UI
kubectl port-forward svc/rabbitmq 15672:15672 &

# Publish 20 messages qua RabbitMQ HTTP API
for i in $(seq 1 20); do
  curl -s -u user:password -X POST \
    "http://localhost:15672/api/exchanges/%2F/amq.default/publish" \
    -H "Content-Type: application/json" \
    -d "{\"properties\":{},\"routing_key\":\"task-queue\",\"payload\":\"message-$i\",\"payload_encoding\":\"string\"}"
done
echo "Published 20 messages"

# BƯỚC 5: Quan sát KEDA scale up
kubectl get pods -l app=message-consumer -w
# Sau ~10-20s:
# message-consumer-xxx  0/1  ContainerCreating  0  5s
# message-consumer-xxx  1/1  Running            0  8s
# message-consumer-yyy  0/1  ContainerCreating  0  10s
# ... đến 4 pods (20 messages / 5 per pod)
# BƯỚC 6: Quan sát scale-down về 0 sau khi queue rỗng
# Queue rỗng (messages đã expire hoặc được consume)
# Sau cooldownPeriod=30s, KEDA scale xuống 0
kubectl get pods -l app=message-consumer
# Pods: 0 — scale-to-zero hoạt động!

# Kiểm tra HPA mà KEDA tạo ra:
kubectl get hpa keda-hpa-rabbitmq-scaledobject
# MINPODS=0, MAXPODS=10

✅ Kết quả mong đợi: Consumer pods tăng từ 0 lên ~4 sau khi publish 20 messages (cứ 5 msg = 1 pod). Sau khi queue rỗng và cooldown, pods về 0. kubectl get scaledobject hiển thị READY=True và ACTIVE đổi từ True sang False khi scale về 0.

🧹 Cleanup:

kubectl delete -f keda-scaledobject.yaml
kubectl delete -f keda-consumer.yaml
kubectl delete secret rabbitmq-secret
helm uninstall rabbitmq
helm uninstall keda -n keda
kill $(lsof -t -i:15672) 2>/dev/null || true
LAB-160

Guardrail — Auto-remediation yêu cầu Approval cho hành động nguy hiểm

Python · Slack Webhook · kubectl

🎯 Mục tiêu: Xây dựng guardrail logic: hành động an toàn (restart) thực thi ngay; hành động nguy hiểm (scale down, delete) phải gửi thông báo và chờ approval trước khi thực thi.

🧰 Công cụ / nền tảng: Python, Slack Incoming Webhook (hoặc file-based mock), kubectl.

📦 Chuẩn bị: Python 3.8+, pip install flask requests. Tùy chọn: Slack workspace với Incoming Webhook URL.

▶️ Các bước:

# BƯỚC 1: Tạo guardrail webhook server
cat > guardrail_webhook.py << 'PYEOF'
#!/usr/bin/env python3
"""
Guardrail auto-remediation webhook.
- SAFE actions: thực thi ngay (restart, scale-up)
- DANGEROUS actions: notify + ghi pending approval file, block thực thi
"""
from flask import Flask, request, jsonify
import subprocess, json, os, time, logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')

PENDING_DIR = "/tmp/remediation-pending"
os.makedirs(PENDING_DIR, exist_ok=True)

# Phân loại mức độ nguy hiểm
ACTION_POLICY = {
    "PodCrashLooping":   {"risk": "safe",      "action": "rollout_restart"},
    "HighErrorRate":     {"risk": "safe",      "action": "rollout_restart"},
    "HighMemoryUsage":   {"risk": "safe",      "action": "scale_up"},
    "DiskSpaceCritical": {"risk": "dangerous", "action": "cleanup_pvc"},
    "NodeUnhealthy":     {"risk": "dangerous", "action": "drain_node"},
}

SLACK_WEBHOOK = os.getenv("SLACK_WEBHOOK_URL", "")

def notify(message: str):
    """Gửi notification — Slack nếu có config, nếu không thì log"""
    app.logger.warning(f"NOTIFY: {message}")
    if SLACK_WEBHOOK:
        import requests
        requests.post(SLACK_WEBHOOK, json={"text": message}, timeout=5)
    # Ghi vào file để test không cần Slack
    with open(f"{PENDING_DIR}/notifications.log", "a") as f:
        f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n")

def execute_safe_action(action: str, deployment: str, namespace: str = "default") -> dict:
    if action == "rollout_restart":
        result = subprocess.run(
            ["kubectl", "rollout", "restart", f"deployment/{deployment}", "-n", namespace],
            capture_output=True, text=True, timeout=30
        )
        return {"executed": action, "rc": result.returncode, "out": result.stdout.strip()}
    elif action == "scale_up":
        result = subprocess.run(
            ["kubectl", "scale", f"deployment/{deployment}", "--replicas=3", "-n", namespace],
            capture_output=True, text=True, timeout=30
        )
        return {"executed": action, "rc": result.returncode, "out": result.stdout.strip()}
    return {"error": f"unknown action: {action}"}

def queue_for_approval(alert_name: str, action: str, target: str) -> dict:
    """Ghi pending action vào file, chờ human approve"""
    pending_id = f"{int(time.time())}-{alert_name}"
    pending_file = f"{PENDING_DIR}/{pending_id}.json"
    pending = {
        "id": pending_id,
        "alert": alert_name,
        "action": action,
        "target": target,
        "queued_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "status": "pending_approval"
    }
    with open(pending_file, "w") as f:
        json.dump(pending, f, indent=2)

    notify(
        f":warning: *DANGEROUS action requires approval*\n"
        f"Alert: `{alert_name}` | Action: `{action}` | Target: `{target}`\n"
        f"Approval ID: `{pending_id}`\n"
        f"To approve: POST /approve/{pending_id}"
    )
    return {"queued": True, "pending_id": pending_id, "status": "awaiting_approval"}

@app.route('/webhook', methods=['POST'])
def handle_alert():
    data = request.get_json(force=True)
    results = []
    for alert in data.get('alerts', []):
        alert_name = alert.get('labels', {}).get('alertname', '')
        deployment = alert.get('labels', {}).get('deployment', 'unknown')
        status = alert.get('status', '')
        if status != 'firing':
            continue

        policy = ACTION_POLICY.get(alert_name)
        if not policy:
            results.append({"alert": alert_name, "skipped": "no policy"})
            continue

        if policy["risk"] == "safe":
            result = execute_safe_action(policy["action"], deployment)
            app.logger.info(f"Safe action executed: {result}")
            results.append(result)
        else:
            result = queue_for_approval(alert_name, policy["action"], deployment)
            app.logger.warning(f"Dangerous action queued: {result}")
            results.append(result)

    return jsonify({"results": results}), 200

@app.route('/approve/', methods=['POST'])
def approve_action(pending_id: str):
    """Endpoint để human approve pending action"""
    pending_file = f"{PENDING_DIR}/{pending_id}.json"
    if not os.path.exists(pending_file):
        return jsonify({"error": "pending action not found"}), 404

    with open(pending_file) as f:
        pending = json.load(f)

    app.logger.warning(f"APPROVED by human: {pending_id} → {pending['action']} on {pending['target']}")
    # Thực thi action sau khi được approve
    result = {"approved": pending_id, "action": pending["action"], "note": "executed after human approval"}
    pending["status"] = "approved_and_executed"
    with open(pending_file, "w") as f:
        json.dump(pending, f, indent=2)
    return jsonify(result), 200

@app.route('/pending', methods=['GET'])
def list_pending():
    """Liệt kê các action đang chờ approval"""
    items = []
    for fname in os.listdir(PENDING_DIR):
        if fname.endswith(".json"):
            with open(f"{PENDING_DIR}/{fname}") as f:
                items.append(json.load(f))
    return jsonify({"pending": items}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5002)
PYEOF

python guardrail_webhook.py &
echo "Guardrail webhook running on :5002"
# BƯỚC 2: Test SAFE action (PodCrashLooping → restart ngay)
curl -s -X POST http://localhost:5002/webhook \
  -H "Content-Type: application/json" \
  -d '{
    "alerts": [{
      "status": "firing",
      "labels": {
        "alertname": "PodCrashLooping",
        "deployment": "test-app",
        "severity": "critical"
      }
    }]
  }' | python3 -m json.tool
# Expected: {"results": [{"executed": "rollout_restart", "rc": 0, ...}]}
# BƯỚC 3: Test DANGEROUS action (NodeUnhealthy → queue, không tự động chạy)
curl -s -X POST http://localhost:5002/webhook \
  -H "Content-Type: application/json" \
  -d '{
    "alerts": [{
      "status": "firing",
      "labels": {
        "alertname": "NodeUnhealthy",
        "deployment": "worker-node-1",
        "severity": "critical"
      }
    }]
  }' | python3 -m json.tool
# Expected: {"results": [{"queued": true, "pending_id": "...", "status": "awaiting_approval"}]}

# Xem pending actions:
curl -s http://localhost:5002/pending | python3 -m json.tool

# BƯỚC 4: Human approve hành động nguy hiểm
PENDING_ID=$(curl -s http://localhost:5002/pending | python3 -c \
  "import sys,json; data=json.load(sys.stdin); print(data['pending'][0]['id'])")
echo "Approving: $PENDING_ID"
curl -s -X POST "http://localhost:5002/approve/$PENDING_ID" | python3 -m json.tool
# Expected: {"approved": "...", "action": "drain_node", "note": "executed after human approval"}

# BƯỚC 5: Xem log notifications
cat /tmp/remediation-pending/notifications.log

✅ Kết quả mong đợi: Safe actions (PodCrashLooping) thực thi ngay, trả về kết quả kubectl. Dangerous actions (NodeUnhealthy) được queue với ID, ghi notification log, không thực thi cho đến khi có approval POST request. /pending endpoint liệt kê đúng các pending items.

🧹 Cleanup:

kill $(lsof -t -i:5002) 2>/dev/null || true
rm -rf /tmp/remediation-pending guardrail_webhook.py

3. Tình huống doanh nghiệp thực tế

Bối cảnh

Một e-commerce platform có flash sale vào 12h trưa và 8h tối mỗi ngày. Traffic tăng đột biến 10x trong 30 phút. On-call engineer không thể luôn sẵn sàng để scale thủ công. Hệ thống cũng gặp memory leak định kỳ khiến một số pods cần restart.

Giải pháp NoOps

  • Pre-emptive scaling với KEDA cron trigger: ScaledObject cron trigger scale từ 5 → 30 pods trước 11:55 mỗi ngày; scale down về 5 pods lúc 13:00 và 21:00. Zero manual intervention.
  • Liveness probe cho memory leak: /healthz endpoint check heap usage; nếu vượt 85% limit → trả về 503 → K8s restart pod. Pod mới khởi động với heap sạch. MTTR từ 15 phút (alert → pager → SSH → restart) xuống <60 giây (tự động).
  • Auto-rollback trong CI/CD: Script LAB-158 tích hợp vào GitHub Actions; mọi deploy production đều có safety net rollback tự động. Change Failure Rate giảm vì lỗi được tự phát hiện và revert trong <2 phút.
  • Guardrail cho actions nguy hiểm: Node drain và PVC deletion yêu cầu Slack approval; engineer click "Approve" trong Slack message. Giảm thiểu sự cố do auto-remediation thực thi sai context.
  • Kết quả đo được: On-call pages giảm 70%; MTTR giảm từ 18 phút xuống 90 giây cho sự cố đã biết; chi phí compute giảm 35% nhờ scale-to-zero ngoài giờ cao điểm.

📚 Nguồn tham khảo

Module 31: Backstage Developer Portal Module 33: ChatOps & AIOps
Zalo