Tạo Webhook Tự Động Deploy: Hết Cảnh SSH Lúc 2 Giờ Sáng

Development tutorial - IT technology blog
Development tutorial - IT technology blog

2 giờ sáng. Khách hàng nhắn tin báo lỗi. Mình debug xong, push fix lên GitHub, rồi lại mở terminal: ssh root@server, rồi cd /var/www/app && git pull && npm install && pm2 restart app. Xong lúc 3 giờ, đi ngủ. Hôm sau lại làm y chang. Cái vòng lặp này kéo dài mấy tháng cho đến khi mình chịu khó ngồi setup webhook một lần.

Dự án web app gần nhất có 5 developer — sau khi áp dụng quy trình này, mọi thứ đổi khác hẳn. Từ lúc push code đến khi production cập nhật chỉ còn dưới 60 giây. Không cần ai ngồi canh. Không ai quên deploy vì đang buồn ngủ.

Webhook hoạt động như thế nào

Đơn giản thôi: webhook là một HTTP endpoint trên server của bạn. Khi bạn push code lên GitHub, GitHub gửi một POST request đến endpoint đó kèm thông tin về commit. Server nhận được, verify xem có đúng từ GitHub không, rồi chạy deploy script.

GitHub Actions đòi bạn viết YAML, cấu hình runner, quản lý secrets/environments. Jenkins còn nặng hơn. Webhook thì không — đủ đơn giản để tự dựng trong một buổi sáng, đủ ổn định để chạy production nhiều tháng không cần đụng lại.

Developer push code lên GitHub
        ↓
GitHub gửi POST → https://yourserver.com/webhook
        ↓
Webhook server verify secret key
        ↓
Chạy: git pull → install deps → restart service
        ↓
Production cập nhật ✓

Thực hành: Dựng webhook từ đầu

Bước 1: Viết webhook server bằng Python

Mình chọn Python + Flask — gọn, dễ debug lúc đêm khuya, và không cần học gì thêm nếu bạn đã biết Python. Cài Flask trên server trước:

pip3 install flask

Tạo file /var/www/webhook/webhook.py:

import hmac
import hashlib
import subprocess
import os
from flask import Flask, request, abort

app = Flask(__name__)

# Lấy từ biến môi trường, KHÔNG hardcode vào code
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "").encode()
DEPLOY_SCRIPT = "/var/www/webhook/deploy.sh"

def verify_signature(payload_body, signature_header):
    """Xác thực request đến từ GitHub, không phải ai đó random."""
    if not signature_header:
        return False
    hash_object = hmac.new(WEBHOOK_SECRET, msg=payload_body, digestmod=hashlib.sha256)
    expected = "sha256=" + hash_object.hexdigest()
    return hmac.compare_digest(expected, signature_header)

@app.route("/webhook", methods=["POST"])
def webhook():
    signature = request.headers.get("X-Hub-Signature-256")
    if not verify_signature(request.data, signature):
        abort(403)

    payload = request.json

    # Chỉ deploy khi push lên nhánh main
    if payload.get("ref") != "refs/heads/main":
        return {"status": "skipped", "reason": "not main branch"}, 200

    result = subprocess.run(
        [DEPLOY_SCRIPT],
        capture_output=True,
        text=True,
        timeout=120
    )

    if result.returncode != 0:
        print(f"Deploy failed:\n{result.stderr}")
        return {"status": "error", "message": result.stderr}, 500

    return {"status": "success"}, 200

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=9000)

Bước 2: Viết deploy script

Tạo file /var/www/webhook/deploy.sh. Script này là nơi bạn viết logic deploy thực tế — git pull, cài deps, restart service:

#!/bin/bash
set -e  # Dừng ngay nếu bất kỳ lệnh nào lỗi

APP_DIR="/var/www/myapp"
LOG_FILE="/var/log/deploy.log"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting deploy..." >> $LOG_FILE

cd $APP_DIR
git pull origin main >> $LOG_FILE 2>&1
npm install --production >> $LOG_FILE 2>&1
pm2 restart myapp >> $LOG_FILE 2>&1

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy completed ✓" >> $LOG_FILE
chmod +x /var/www/webhook/deploy.sh

Bước 3: Chạy webhook server với systemd

Đừng chạy Flask bằng python webhook.py & rồi để đó — process sẽ chết khi server reboot, và bạn sẽ mất một lúc mới nhận ra tại sao deploy không chạy nữa. Dùng systemd để nó tự restart:

sudo nano /etc/systemd/system/webhook.service
[Unit]
Description=GitHub Webhook Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/webhook
Environment="WEBHOOK_SECRET=your_secret_here"
ExecStart=/usr/bin/python3 /var/www/webhook/webhook.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl enable webhook
sudo systemctl start webhook
sudo systemctl status webhook

Bước 4: Expose qua Nginx

Webhook server đang nghe trên 127.0.0.1:9000. Thêm location vào Nginx config để GitHub có thể gọi được:

location /webhook {
    proxy_pass http://127.0.0.1:9000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}
sudo nginx -t && sudo systemctl reload nginx

Bước 5: Cấu hình GitHub Webhook

Vào repo GitHub → SettingsWebhooksAdd webhook. Điền các thông tin:

  • Payload URL: https://yourserver.com/webhook
  • Content type: application/json
  • Secret: Chuỗi ngẫu nhiên — phải khớp với WEBHOOK_SECRET trong systemd service
  • Events: Chọn Just the push event là đủ

Để tạo secret an toàn:

openssl rand -hex 32

Kiểm tra hoạt động

GitHub có nút Redeliver trong tab Webhooks để gửi lại request test — tiện hơn là push một commit rác chỉ để thử. Hoặc push một commit nhỏ lên nhánh main rồi theo dõi:

tail -f /var/log/deploy.log

Thấy dòng “Deploy completed ✓” là xong. Nếu không thấy, log của systemd thường chỉ thẳng lỗi ở đâu:

journalctl -u webhook -f

Các lỗi hay gặp và cách fix

403 Forbidden — Secret key không khớp. Kiểm tra lại WEBHOOK_SECRET trong systemd service có giống với secret điền trên GitHub không. Sửa xong nhớ reload:

sudo systemctl daemon-reload && sudo systemctl restart webhook

Permission denied khi chạy git pull — User www-data không có quyền với thư mục app. Fix một lệnh:

sudo chown -R www-data:www-data /var/www/myapp

Git hỏi password — Server chưa cấu hình SSH key với GitHub. Cách nhanh là dùng Personal Access Token trực tiếp trong URL remote:

git remote set-url origin https://[email protected]/username/repo.git

Deploy script timeout — Script bị kill sau 120 giây theo mặc định. Nếu deploy của bạn mất lâu hơn — build Docker image, chạy database migration — tăng giá trị timeout trong subprocess.run() cho phù hợp.

Kết luận

Về bản chất, webhook chỉ là một HTTP server nhỏ ngồi chờ tín hiệu từ GitHub rồi chạy script. Không có gì phức tạp. Nhưng nó cắt đứt cái vòng lặp SSH-pull-restart mà mình (và có lẽ bạn) đã tốn không biết bao nhiêu đêm.

Khi nào cần rollback tự động, deploy nhiều môi trường, hay thông báo Slack khi có sự cố — lúc đó mới tính đến GitHub Actions hoặc các platform CI/CD chuyên dụng. Còn bây giờ, với project cần chạy ngay hôm nay: cách này đủ dùng, ít code, ít thứ có thể vỡ.

Share: