CI/CD Tetikleme: Webhook ile Otomatik Deployment
Ekibin her push yaptığında sunucuya SSH açıp elle git pull && npm run build yazmaktan bıktıysanız, doğru yerdesiniz. Webhook tabanlı CI/CD sistemi kurduğunuzda bu işlemlerin otomatik çalıştığını ilk gördüğünüzde gerçekten küçük bir mutluluk hissediyorsunuz. Bu yazıda sıfırdan çalışan bir webhook altyapısı kuracağız, güvenliğini sağlayacağız ve gerçek deployment senaryolarında nasıl kullanacağımızı göreceğiz.
Webhook Nedir ve Neden Önemlidir
Webhook’u en basit şekilde şöyle tarif edebiliriz: bir olay gerçekleştiğinde sistemin size HTTP isteği atması. Siz sürekli “bir şey değişti mi?” diye sormak yerine, sistem “değişti, işte bilgi” diyor. Bu pull yerine push modelidir.
GitHub’a kod push ettiğinizde, GitHub sizin belirlediğiniz bir URL’ye POST isteği atar. Bu istek içinde kim ne zaman hangi branch’e ne commitledi, tüm bunlar JSON formatında geliyor. Sizin sunucunuzdaki küçük bir dinleyici script bu isteği karşılıyor, doğruluğunu kontrol ediyor ve deployment script’ini tetikliyor.
Alternatif olan polling yönteminde ise sunucunuz her 30 saniyede bir GitHub’a “yeni bir şey var mı?” diye sormak zorunda kalır. Bu hem kaynak israfıdır hem de anlık tepki yerine gecikme yaratır.
Genel Mimari
Tipik bir webhook tabanlı deployment akışı şöyle işler:
- Developer kodu feature branch’ten main’e merge eder
- GitHub/GitLab/Bitbucket webhook endpoint’inize POST atar
- Sunucunuzdaki webhook listener isteği alır
- İmza doğrulaması yapılır (güvenlik kritik)
- Deployment script çalışır: pull, build, restart
- Sonuç loglara yazılır, opsiyonel olarak Slack’e bildirim gider
Bu akışı hayata geçirmek için birkaç farklı yol var. Ben hem basit bir Python listener hem de production’da daha sağlam olan webhook aracını göstereceğim.
Sunucu Hazırlığı
Önce sistemimizi hazırlayalım. Ubuntu 22.04 üzerinde çalışıyorum ama adımlar Debian ve RHEL tabanlı sistemlerde de benzer.
# Gerekli paketleri kur
sudo apt update
sudo apt install -y git curl python3 python3-pip nginx
# Deployment için ayrı bir kullanıcı oluştur
sudo useradd -m -s /bin/bash deployer
sudo usermod -aG www-data deployer
# Deployment dizinini hazırla
sudo mkdir -p /var/www/myapp
sudo chown deployer:deployer /var/www/myapp
# deployer kullanıcısı için SSH key oluştur (GitHub deploy key için)
sudo -u deployer ssh-keygen -t ed25519 -C "deploy@myserver" -f /home/deployer/.ssh/id_ed25519 -N ""
GitHub’a gidip repo ayarlarından “Deploy keys” bölümüne /home/deployer/.ssh/id_ed25519.pub içeriğini ekleyin. Bu sayede deployer kullanıcısı repo’yu klonlayıp çekebilir ama yazmaya yetkisi olmaz.
Basit Python Webhook Listener
Önce işin mantığını anlamak için sade bir Python listener yazalım. Production için değil, kavramı kavramak için ideal.
#!/usr/bin/env python3
# /home/deployer/webhook_listener.py
import hmac
import hashlib
import subprocess
import logging
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import os
SECRET = os.environ.get('WEBHOOK_SECRET', 'degistirmeniz-lazim')
DEPLOY_SCRIPT = '/home/deployer/deploy.sh'
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
handlers=[
logging.FileHandler('/var/log/webhook/deploy.log'),
logging.StreamHandler()
]
)
class WebhookHandler(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length)
# GitHub imza doğrulaması
signature_header = self.headers.get('X-Hub-Signature-256', '')
expected_sig = 'sha256=' + hmac.new(
SECRET.encode(), body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_sig, signature_header):
logging.warning(f"Gecersiz imza: {self.client_address}")
self.send_response(401)
self.end_headers()
return
payload = json.loads(body)
ref = payload.get('ref', '')
# Sadece main branch'i dinle
if ref != 'refs/heads/main':
logging.info(f"Atlandi: {ref} branch push'u")
self.send_response(200)
self.end_headers()
self.wfile.write(b'Skipped')
return
logging.info(f"Deployment tetiklendi: {payload.get('head_commit', {}).get('message', '')}")
# Deploy script'i arka planda çalıştır
subprocess.Popen(['/bin/bash', DEPLOY_SCRIPT],
stdout=open('/var/log/webhook/deploy.log', 'a'),
stderr=subprocess.STDOUT)
self.send_response(200)
self.end_headers()
self.wfile.write(b'Deployment started')
def log_message(self, format, *args):
pass # HTTP access logunu sustur
if __name__ == '__main__':
os.makedirs('/var/log/webhook', exist_ok=True)
server = HTTPServer(('127.0.0.1', 9000), WebhookHandler)
logging.info("Webhook listener 9000 portunda basladi")
server.serve_forever()
Deployment Script
Webhook tetiklendiğinde çalışacak asıl script:
#!/bin/bash
# /home/deployer/deploy.sh
set -euo pipefail
APP_DIR="/var/www/myapp"
LOG_FILE="/var/log/webhook/deploy.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() {
echo "[$TIMESTAMP] $1" | tee -a "$LOG_FILE"
}
log "=== Deployment basliyor ==="
cd "$APP_DIR"
# Mevcut commit hash'ini kaydet (rollback için)
PREV_COMMIT=$(git rev-parse HEAD)
log "Onceki commit: $PREV_COMMIT"
# Kodu çek
log "Git pull yapiliyor..."
git fetch origin main
git reset --hard origin/main
NEW_COMMIT=$(git rev-parse HEAD)
log "Yeni commit: $NEW_COMMIT"
# Node.js uygulaması örneği
if [ -f "package.json" ]; then
log "npm install yapiliyor..."
npm ci --production
log "Build aliniyor..."
npm run build
log "Uygulama yeniden baslatiliyor..."
pm2 restart myapp --update-env || pm2 start ecosystem.config.js
# Health check
sleep 3
if curl -sf http://localhost:3000/health > /dev/null; then
log "Health check basarili"
else
log "HATA: Health check basarisiz, rollback yapiliyor..."
git reset --hard "$PREV_COMMIT"
npm ci --production && npm run build
pm2 restart myapp
exit 1
fi
fi
log "=== Deployment tamamlandi ==="
# Slack bildirimi (opsiyonel)
if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then
curl -s -X POST "$SLACK_WEBHOOK_URL"
-H 'Content-Type: application/json'
-d "{"text": "Deployment basarili: $(git log -1 --pretty=%s)"}"
fi
Script’i çalıştırılabilir yapın:
chmod +x /home/deployer/deploy.sh
sudo mkdir -p /var/log/webhook
sudo chown deployer:deployer /var/log/webhook
adım: adım Adım Adhocd Araç Kullanımı: webhook
Gerçek production ortamlarında adnanh/webhook aracı çok daha uygun. Go ile yazılmış, tek binary, systemd ile güzel çalışıyor.
# webhook binary'sini indir
cd /tmp
WEBHOOK_VERSION="2.8.1"
wget "https://github.com/adnanh/webhook/releases/download/${WEBHOOK_VERSION}/webhook-linux-amd64.tar.gz"
tar xzf webhook-linux-amd64.tar.gz
sudo mv webhook-linux-amd64/webhook /usr/local/bin/webhook
sudo chmod +x /usr/local/bin/webhook
# Konfigürasyon dizini oluştur
sudo mkdir -p /etc/webhook
Şimdi webhook konfigürasyon dosyasını oluşturalım:
[
{
"id": "deploy-myapp",
"execute-command": "/home/deployer/deploy.sh",
"command-working-directory": "/var/www/myapp",
"response-message": "Deployment started",
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "buraya-gizli-anahtarinizi-yazin",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
},
"pass-environment-to-command": [
{
"source": "payload",
"name": "head_commit.message",
"envname": "COMMIT_MESSAGE"
}
]
}
]
Bu JSON konfigürasyonu /etc/webhook/hooks.json olarak kaydedin. Konfigürasyon şunu söylüyor: gelen istek SHA-256 imzasını geçerse ve ref değeri refs/heads/main ise deploy script’i çalıştır.
Systemd Servis Olarak Yapılandırma
# /etc/systemd/system/webhook.service
sudo tee /etc/systemd/system/webhook.service << 'EOF'
[Unit]
Description=Webhook Listener
After=network.target
[Service]
Type=simple
User=deployer
Group=deployer
ExecStart=/usr/local/bin/webhook
-hooks /etc/webhook/hooks.json
-port 9000
-ip 127.0.0.1
-hotreload
-verbose
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
EnvironmentFile=-/etc/webhook/webhook.env
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable webhook
sudo systemctl start webhook
sudo systemctl status webhook
/etc/webhook/webhook.env dosyasına Slack URL gibi hassas değişkenleri koyabilirsiniz:
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXX/YYYY/ZZZZ
Nginx Reverse Proxy Yapılandırması
Webhook listener’ı doğrudan internete açmak yerine Nginx arkasına alın. Bu sayede SSL terminasyonu, rate limiting ve loglama Nginx tarafında yapılır.
# /etc/nginx/sites-available/webhook
server {
listen 443 ssl http2;
server_name deploy.mycompany.com;
ssl_certificate /etc/letsencrypt/live/deploy.mycompany.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/deploy.mycompany.com/privkey.pem;
# Sadece GitHub IP aralıklarına izin ver (opsiyonel ama önerilen)
# allow 192.30.252.0/22;
# allow 185.199.108.0/22;
# allow 140.82.112.0/20;
# deny all;
# Rate limiting: dakikada max 10 istek
limit_req_zone $binary_remote_addr zone=webhook:10m rate=10r/m;
limit_req zone=webhook burst=5 nodelay;
location /hooks/ {
proxy_pass http://127.0.0.1:9000/hooks/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Webhook payload boyutunu sınırla
client_max_body_size 1m;
# Timeout ayarları
proxy_connect_timeout 10s;
proxy_read_timeout 30s;
}
# Diğer tüm istekleri reddet
location / {
return 404;
}
}
GitHub Webhook Kurulumu
GitHub tarafında ayarlamak için:
- Repo’nuzda Settings > Webhooks > Add webhook yolunu izleyin
- Payload URL:
https://deploy.mycompany.com/hooks/deploy-myapp - Content type:
application/json - Secret:
/etc/webhook/hooks.jsoniçine yazdığınız secret ile aynı olmalı - Which events: Sadece “Pushes” seçin, gereksiz trafikten kaçının
- Active: Işaretli olsun
Secret oluşturmak için güvenli bir yol:
# Rastgele güçlü bir secret üret
openssl rand -hex 32
# Çıktı örneği: a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
GitLab için Uyarlama
GitLab webhook’ları biraz farklı header kullanır. Konfigürasyonu buna göre düzenleyin:
[
{
"id": "deploy-myapp-gitlab",
"execute-command": "/home/deployer/deploy.sh",
"trigger-rule": {
"and": [
{
"match": {
"type": "value",
"value": "gizli-token-degeriniz",
"parameter": {
"source": "header",
"name": "X-Gitlab-Token"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
GitLab’da Settings > Webhooks altında URL ve token alanlarını doldurmanız yeterli. GitLab HMAC yerine düz token doğrulama kullandığı için secret değeri header’da doğrudan gelir.
Güvenlik Kontrol Listesi
Production’a geçmeden önce şunları mutlaka kontrol edin:
- HTTPS zorunlu: Webhook payload’larını şifresiz HTTP üzerinden asla taşımayın
- Secret validation: HMAC imza doğrulamasını asla atlamamak
- Timing-safe comparison: Python’da
hmac.compare_digest, Go’da zaten webhook yapar. Normal string karşılaştırması timing attack’a açık - Minimum yetki: deployer kullanıcısının sadece deployment için gereken yetkisi olsun, sudo vermek zorunda değilsiniz
- Log rotasyonu:
/etc/logrotate.d/webhookdosyası oluşturun - IP whitelist: GitHub/GitLab’ın IP aralıklarını Nginx veya iptables seviyesinde kısıtlamak ek güvenlik sağlar
- Payload boyutu sınırı: Büyük payload’lar ile DoS saldırısına karşı client_max_body_size kullanın
# Logrotate konfigürasyonu
sudo tee /etc/logrotate.d/webhook << 'EOF'
/var/log/webhook/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 deployer deployer
}
EOF
Çoklu Environment Yönetimi
Gerçek dünyada staging ve production için farklı branch’ler tetiklemek isteyebilirsiniz. hooks.json dosyasına birden fazla hook ekleyebilirsiniz:
[
{
"id": "deploy-staging",
"execute-command": "/home/deployer/deploy-staging.sh",
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "staging-secret-buraya",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/develop",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
},
{
"id": "deploy-production",
"execute-command": "/home/deployer/deploy-production.sh",
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "production-secret-buraya",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
Staging için URL https://deploy.mycompany.com/hooks/deploy-staging, production için https://deploy.mycompany.com/hooks/deploy-production olacak. GitHub’da her repo için iki ayrı webhook tanımlamanız yeterli.
Test ve Hata Ayıklama
Webhook’un düzgün çalışıp çalışmadığını test etmek için:
# Manuel olarak webhook tetikle (test amaçlı)
SECRET="sizin-secret-degeriniz"
PAYLOAD='{"ref":"refs/heads/main","head_commit":{"message":"test commit"}}'
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/.*= //')
curl -X POST https://deploy.mycompany.com/hooks/deploy-myapp
-H "Content-Type: application/json"
-H "X-Hub-Signature-256: sha256=$SIG"
-H "X-GitHub-Event: push"
-d "$PAYLOAD"
# Logları takip et
sudo journalctl -u webhook -f
# Deployment loglarını izle
tail -f /var/log/webhook/deploy.log
GitHub’ın webhook ayarları sayfasında “Recent Deliveries” bölümü de çok işe yarar. Her webhook isteğinin HTTP response kodunu ve body’sini görebilirsiniz. 200 yerine 400 veya 500 görürseniz oradan başlamak mantıklı.
Sonuç
Webhook tabanlı deployment kurduğunuzda ekibin iş akışı gerçekten değişiyor. Pull request merge ediliyor, birkaç saniye içinde staging’e düşüyor, herkes tarayıcıdan kontrol edebiliyor. Production deployment için ek adımlar veya approval mekanizmaları eklemek de mümkün; script’e Slack’e “onaylıyor musunuz?” mesajı atıp cevabı bekleyen bir mekanizma eklemek bile çok uzun sürmez.
En kritik noktaları özetleyecek olursam: HMAC imza doğrulaması olmadan webhook’u asla açmayın, deployment kullanıcısına minimum yetki verin, health check ekleyin ve rollback mekanizmasını baştan planlayın. Deploy script’iniz başarısız olduğunda sistemi çalışır bırakmak, sıfırdan yeniden ayağa kaldırmaktan her zaman daha kolaydır.
Bu altyapıyı bir kez kurduğunuzda neden daha önce yapmadığınızı soracaksınız kendinize.
