1. Einführung: Jetzt ist es Zeit, den Code zu bearbeiten
Hallo! Im ersten Teil haben wir die Gründe, why wir ein automatisches Deployment-System mit GitHub Webhooks selbst aufbauen wollen und was dafür benötigt wird, erörtert. Im zweiten Teil haben wir die gesamte Architektur und die Kernfunktionen des FastAPI-Webhooks-Services detailliert betrachtet. Jetzt ist es an der Zeit, die im Kopf skizzierte Idee in echten Code umzusetzen.
Wer den vorherigen Teil noch nicht gesehen hat, kann dies über die folgenden Links nachholen.
Aufbau eines automatisierten Deployment-Systems mit GitHub Webhook
1 - Warum selbst implementieren? 2 - Gesamtarchitektur und Prozessdesign
Im dritten Teil werden wir die grundlegenden Einstellungen der Staging-Server-Umgebung abschließen und das Grundgerüst des FastAPI-Webhooks-Servers schaffen, das in der Lage ist, GitHub Webhook-Anfragen sicher zu empfangen und zu verifizieren. Es ist an der Zeit, dass das zuvor vage wir wirkende automatische Deployment-System einen Schritt näher zur Realität rückt!
2. Grundkonfiguration des Staging-Servers
Das Erste, was zu tun ist, besteht darin, die Umgebung des Staging-Servers vorzubereiten, auf dem das Deployment stattfinden soll, damit wir den Code ausführen können. Melden Sie sich via SSH auf dem Server an und folgen Sie den nächsten Schritten.
2.1. Vorbereitungen der Python-Entwicklungsumgebung
Das Einrichten einer Python-virtuellen Umgebung auf dem Server ist eine gute Gewohnheit, um Abhängigkeitskonflikte zwischen Projekten zu vermeiden und eine saubere Entwicklungsumgebung zu erhalten. Installieren Sie die benötigten Pakete mit pip in der venv-Umgebung.
# Installation von FastAPI, Uvicorn und python-dotenv zum Einlesen von .env-Dateien
pip install fastapi uvicorn python-dotenv
2.2. Überprüfung der Installation von Git, Docker und Docker Compose
Wie bereits im letzten Teil erwähnt, sind Git, Docker und Docker Compose die wichtigsten Werkzeuge für automatisierte Deployments. Wir gehen davon aus, dass diese Tools bereits auf dem Server installiert sind, wenn Sie diesen Artikel lesen.
Falls sie nicht installiert sind, überprüfen Sie zuerst mit den folgenden Befehlen, ob dies der Fall ist.
git --version
docker --version
docker compose version # oder docker-compose --version (je nach Installationsmethode unterschiedlich)
Wenn sie nicht installiert sind, folgen Sie bitte den offiziellen Dokumentationen der jeweiligen Tools zur Installation. Besonders für detaillierte Anleitungen zur Docker-Installation können Sie den folgenden Beitrag beachten: Docker Installationsanleitung
Hinweis: Nach der Installation von Docker müssen Sie den aktuellen Benutzer zur Docker-Gruppe hinzufügen und sich abmelden und wieder anmelden, um ohne sudo
den docker
Befehl verwenden zu können.
sudo usermod -aG docker $USER
Nach dem Hinzufügen der Docker-Rechte müssen Sie sich unbedingt vom Server abmelden und wieder anmelden, damit die Änderungen wirksam werden. Testen Sie, ob der Befehl docker run hello-world
ohne sudo
erfolgreich ausgeführt werden kann.
3. Erstellen der Grundstruktur des FastAPI-Webhooks-Servers
Jetzt erstellen wir das Grundgerüst der FastAPI-Anwendung, die Webhook-Anfragen entgegennehmen kann. Erstellen Sie die Datei main.py
im Verzeichnis ~/projects/webhook_server
und schreiben Sie den folgenden Code.
3.1. Erstellen der Datei main.py
und Initialisierung der FastAPI-App
Ich habe zur Erklärung alle Logik in main.py
geschrieben, aber Sie sollten in Ihrem tatsächlichen Projekt zur besseren Wartbarkeit die einzelnen Utility-Funktionen in separate Dateien modularisieren und nach Ihrem Stil übersichtlich gestalten. Hier ist ein Beispiel für die Erstellung.
# ~/projects/webhook_server/main.py
import hmac
import hashlib
import os
import logging
import subprocess
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from dotenv import load_dotenv, dotenv_values
# .env-Datei laden (für die Verwendung von Umgebungsvariablen)
load_dotenv()
# Logging-Einstellungen
logging.basicConfig(
level=logging.INFO, # Protokolliere ab INFO-Level
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("webhook_server.log"), # Protokolle in eine Datei speichern
logging.StreamHandler() # Protokolle auch in der Konsole ausgeben
]
)
logger = logging.getLogger(__name__)
app = FastAPI()
# GitHub Webhook Secret aus den Umgebungsvariablen laden
GITHUB_WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET")
# Test-Root-Endpunkt: In der Produktion ist es ratsam, diesen aus Sicherheitsgründen zu entfernen.
# Sobald es im Internet verfügbar ist, wäre es nicht klug, zu zeigen, dass ein solcher Endpunkt existiert.
@app.get("/")
async def read_root():
return {"message": "Webhook-Server läuft!"}
# Endpunkt zum Empfangen von GitHub-Webhooks
@app.post("/webhook")
async def github_webhook(request: Request, background_tasks: BackgroundTasks):
logger.info("Webhook-Anfrage erhalten.")
# 1. Überprüfung des GitHub Webhook Secrets (der erste Schritt zur Sicherheit)
if GITHUB_WEBHOOK_SECRET:
signature = request.headers.get("X-Hub-Signature-256")
if not signature:
logger.warning("Fehlender Signatur-Header. Abbruch.")
raise HTTPException(status_code=401, detail="X-Hub-Signature-256 Header fehlt")
body = await request.body()
expected_signature = "sha256=" + hmac.new(
GITHUB_WEBHOOK_SECRET.encode('utf-8'),
body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_signature, signature):
logger.warning("Ungültige Signatur. Abbruch.")
raise HTTPException(status_code=401, detail="Ungültige Signatur")
else:
logger.info("Signatur erfolgreich verifiziert.")
else:
logger.warning("GITHUB_WEBHOOK_SECRET ist nicht gesetzt. Überprüfung der Signatur wird übersprungen (NICHT FÜR DIE PRODUKTION EMPFOHLEN).")
# 2. HTTP-Body parsen (Payload)
try:
payload = await request.json()
event_type = request.headers.get("X-GitHub-Event")
# Ziehen des Repository-Namens aus der GitHub Webhook-Payload
# Statt 'full_name' verwenden wir 'name'.
repo_name = payload.get("repository", {}).get("name", "unknown_repository")
ref = payload.get("ref", "unknown_ref")
pusher_name = payload.get("pusher", {}).get("name", "unknown_pusher")
logger.info(f"Ereignis: {event_type}, Repo: {repo_name}, Ref: {ref}, Pusher: {pusher_name}")
# Führen Sie die tatsächliche Deployment-Logik im Hintergrund aus
background_tasks.add_task(handle_deploy, repo_name)
except Exception as e:
logger.error(f"Fehler beim Parsen der Webhook-Payload oder beim Verarbeiten der Anfrage: {e}")
raise HTTPException(status_code=400, detail=f"Fehler bei der Verarbeitung der Anfrage: {e}")
# Sofortige Antwort an GitHub zurückgeben
return {"message": "Webhook empfangen und Verarbeitung im Hintergrund gestartet!"}
# Funktion zur Verarbeitung der tatsächlichen Deployment-Logik
def handle_deploy(repo_name: str):
# Das Repository mit dem Namen 'deployer' wird übersprungen, da es sich um Updates des Webhook-Servers selbst handelt.
if repo_name == "deployer":
logger.info("⚙️ Selbst-Update des Deployer übersprungen.")
return
# Laden Sie die lokalen Pfade der einzelnen Repositories aus den Umgebungsvariablen.
# Dieses Dictionary muss Ihre tatsächlichen Projektnamen und Serverpfade abbilden.
# Beispiel: Der Pfad des Servers für das Projekt muss in der Umgebungsvariable SAMPLE_PROJECT_1_PATH gespeichert sein.
repo_paths = {
"sample_project1": os.getenv("SAMPLE_PROJECT_1_PATH"),
"sample_project2": os.getenv("SAMPLE_PROJECT_2_PATH"),
"sample_project3": os.getenv("SAMPLE_PROJECT_3_PATH"),
}
# Wenn die Namen des Repositories und des Docker Compose-Projekts unterschiedlich sind, empfiehlt es sich, die Namen entsprechend zuzuordnen.
# Das ermöglicht die Verwendung der -p (Projektname) Option von Docker Compose.
# Beachten Sie, dass aufgrund der Eigenschaften von Docker die -p Option keine Großbuchstaben oder Bindestriche enthalten kann.
p_name = {
"sample_project1": "p_name_1",
"sample_project2": "p_name_2",
"sample_project3": "p_name_3",
}
# Finden Sie den lokalen Pfad des Repositories, von dem die Webhook-Anfrage eingegangen ist.
repo_path: Optional[str] = repo_paths.get(repo_name)
# Wenn das Repository nicht definiert ist, protokollieren Sie eine Warnung und beenden Sie den Vorgang.
if not repo_path:
logger.warning(f"🚫 Unbekanntes Repository: {repo_name}")
return
# Konvertieren in ein Path-Objekt für eine einfachere Handhabung der Dateipfade im Dateisystem.
repo_path = Path(repo_path)
# Laden Sie die Werte für DEBUG und COLOR aus der .env-Datei des Repositories.
# Dies kann verwendet werden, um Entwicklungs- und Produktionsumgebungen auf Projektbasis zu unterscheiden oder um den Namen des Docker Compose-Projekts dynamisch festzulegen.
env_path = repo_path / ".env"
env_vars = dotenv_values(env_path) if env_path.exists() else {}
is_debug = env_vars.get("DEBUG", "false").lower() == "true" # Konvertieren des "true"-Strings in einen Boolean
deploy_color = env_vars.get("COLOR", "red") # Standardwert ist "red"
project_name = str(deploy_color) + p_name.get(repo_name, repo_name) # Docker Compose-Projektname
# Je nach Wert von DEBUG kann auch die Docker Compose-Datei bestimmt werden.
docker_compose_file = "docker-compose.dev.yml" if is_debug else "docker-compose.prod.yml"
# Definieren Sie den Pfad zur Docker Compose-Datei.
compose_file = repo_path / docker_compose_file
logger.info(f"Ziehen des neuesten Codes für {repo_name} in {repo_path}...")
# Führen Sie git pull aus: Holen Sie den neuesten Code aus dem entsprechenden Repository-Pfad.
# Die Option check=True wirft eine subprocess.CalledProcessError bei einem fehlerhaften Ausführen des Befehls, was die Ausnahmebehandlung unterstützt.
try:
subprocess.run(["git", "-C", repo_path, "pull"], check=True, capture_output=True, text=True)
logger.info(f"Git pull erfolgreich für {repo_name}.")
except subprocess.CalledProcessError as e:
logger.error(f"Git pull fehlgeschlagen für {repo_name}: {e.stderr}")
return # Beenden Sie hier bei einem Fehler beim Deployment
logger.info(f"Überprüfen auf Änderungen in {repo_name} zur Entscheidung über den Neuaufbau...")
# Vergleich von Änderungen: Bestimmen, ob die Docker-Images neu gebaut werden müssen.
changed = should_rebuild(repo_path)
try:
if changed:
logger.info(f"🚀 Änderungen erkannt. {repo_name} bauen und mit {docker_compose_file} bereitstellen...")
# Wenn Änderungen vorhanden sind, führen Sie Docker Compose mit der --build-Option aus, um die Images neu zu bauen.
subprocess.run(["docker", "compose", "-p", project_name, "-f", compose_file, "up", "-d", "--build"], check=True, capture_output=True, text=True)
logger.info(f"Docker Compose-Bau und hochfahren erfolgreich für {repo_name}.")
else:
logger.info(f"✨ Keine signifikanten Änderungen. Bereitstellen von {repo_name} ohne Neubau der Images mit {docker_compose_file}...")
# Wenn keine Änderungen vorhanden sind, führen Sie Docker Compose ohne die --build-Option aus.
subprocess.run(["docker", "compose", "-p", project_name, "-f", compose_file, "up", "-d"], check=True, capture_output=True, text=True)
logger.info(f"Docker Compose erfolgreich hochgeladen für {repo_name}.")
except subprocess.CalledProcessError as e:
logger.error(f"Docker Compose fehlgeschlagen für {repo_name}: {e.stderr}")
return # Beenden Sie hier bei einem Fehler beim Deployment
logger.info(f"✅ Deployment-Aufgabe für {repo_name} abgeschlossen.")
# Funktion zur Bestimmung, ob Docker-Images neu gebaut werden müssen
def should_rebuild(repo_path: Path) -> bool:
# Diese Dateien erfordern einen Neubau der Docker-Images.
trigger_files = [
"requirements.txt", "Dockerfile", "Dockerfile.dev", "Dockerfile.prod",
".dockerignore", ".env" # Änderungen an .env-Dateien auch als Neubau-Trigger einschließen
]
logger.info(f"Überprüfen des Git-Diffs für {repo_path}...")
# Verwenden Sie Git-Diff, um die Liste der geänderten Dateien zwischen HEAD~1 (letztem Commit) und dem aktuellen HEAD zu ziehen.
# Die Option --name-only bringt nur die Namen der geänderten Dateien.
try:
result = subprocess.run(
["git", "diff", "--name-only", "HEAD~1"],
cwd=repo_path,
capture_output=True, text=True, check=True
)
changed_files_in_git = result.stdout.strip().splitlines()
logger.info(f"Geänderte Dateien in Git: {changed_files_in_git}")
except subprocess.CalledProcessError as e:
logger.error(f"Git-Diff fehlgeschlagen für {repo_path}: {e.stderr}")
# Es ist sicherer, einen Neubau zu versuchen, wenn git diff fehlschlägt.
return True
# Überprüfen, ob sich Trigger-Dateien in der Liste der geänderten Dateien befinden.
if any(f in changed_files_in_git for f in trigger_files):
logger.info("Änderungen an Trigger-Dateien erkannt via git diff. Neubau erforderlich.")
return True
# Überprüfen, ob eine manuelle Neubau-Triggerdatei (REBUILD_TRIGGER) existiert.
# Wenn diese Datei existiert, wird dies als manuelles Neubauen erzwungen und die Datei wird gelöscht.
rebuild_trigger_file = repo_path / "REBUILD_TRIGGER"
if rebuild_trigger_file.exists():
try:
rebuild_trigger_file.unlink() # Datei löschen
logger.info("Erkannte REBUILD_TRIGGER-Datei. Neubau erforderlich und Triggerdatei entfernt.")
return True
except Exception as e:
logger.error(f"Fehler beim Entfernen der REBUILD_TRIGGER-Datei: {e}")
# Es ist sicherer, einen Neubau zu versuchen, auch wenn das Löschen der Datei fehlschlägt.
return True
logger.info("Keine Neubau-Triggerdateien gefunden. Kein Neubau erforderlich.")
return False
3.2. Definition des /webhook
-Endpunkts
In dem obigen Code wird der asynchrone Funktions github_webhook
, definiert unter dem Dekorator app.post("/webhook")
, der Endpunkt sein, um GitHub Webhook-Anfragen zu empfangen. Diese Funktion verarbeitet eingehende POST
-Anfragen und ermöglicht den Zugriff auf Header und Body (Payload) über das Request
-Objekt. BackgroundTasks
ist eine leistungsstarke Funktion von FastAPI, die es ermöglicht, während der sofortigen Antwort an GitHub langwierige Aufgaben wie das Deployment im Hintergrund auszuführen.
4. Implementierung der Logik zur Überprüfung des GitHub Webhook Secret
Das wichtigste Sicherheitselement im obigen Code ist die Logik zur Überprüfung des GITHUB_WEBHOOK_SECRET
.
4.1. Die Bedeutung des Secret
-Wertes
Beim Einrichten des GitHub Webhook können Sie einen Secret
-Wert festlegen. Dieser Wert generiert einen Hash, indem GitHub den Anfrage-Body und den Secret-Wert kombiniert und diesen in der X-Hub-Signature-256
Header sendet. Unser Webhook-Server erzeugt auf die gleiche Weise einen Hash und vergleicht ihn mit dem von GitHub gesendeten Hash, um sicherzustellen, dass die Anfrage tatsächlich von GitHub stammt und dass die Daten während der Übertragung nicht manipuliert wurden.
4.2. Überprüfung mittels des X-Hub-Signature-256
Headers
Wie im Code zu sehen ist, führen wir die Secret-Überprüfung mit den Modulen hmac
und hashlib
durch.
- Secret festlegen: Erstellen Sie zunächst eine
.env
-Datei im Verzeichnis~/projects/webhook_server
und tragen Sie den Secret-Wert ein, der bei der Einrichtung des GitHub-Webhooks verwendet werden soll. Diese Datei sollte in.gitignore
aufgenommen werden, um sicherzustellen, dass sie nicht im Git-Repository landet. Dieser Secret-Wert wird später in den Webhook-Einstellungen des GitHub-Repositories verwendet.
# ~/projects/webhook_server/.env
GITHUB_WEBHOOK_SECRET="Bitte_geben_Sie_hier_den_Secret_Wert_für_die_GitHub_Webhook_Einrichtung_ein"
- FastAPI-Code: In
main.py
verwenden wir diepython-dotenv
-Bibliothek, um diese.env
-Datei zu laden und denGITHUB_WEBHOOK_SECRET
-Wert mitos.getenv()
abzurufen. Danach wird bei jedem Webhook-Request der Signaturwert überRequest.headers.get("X-Hub-Signature-256")
abgerufen und die erwartete Signaturwert wird mithilfe vonhmac.new()
berechnet und verglichen.
Diese Überprüfungslogik ist ein essentieller Schutzschild gegen externe Angriffe auf den Webhook-Endpunkt, also überspringen Sie das auf keinen Fall!
5. Einrichtung eines Logging-Systems
Im Code sehen wir, dass das logging
-Modul für grundlegende Logging-Einstellungen verwendet wurde.
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("webhook_server.log"), # speichern in der webhook_server.log Datei
logging.StreamHandler() # auch in der Konsole ausgeben
]
)
logger = logging.getLogger(__name__)
Mit diesen Einstellungen werden bei jedem eingehenden Webhook-Request Informationen in die webhook_server.log
-Datei geschrieben und gleichzeitig im Terminal angezeigt. Wenn die Deployment-Logik komplizierter wird, können Sie durch diese Logging-Funktion leicht herausfinden, an welchem Schritt ein Problem aufgetreten ist.
6. Ausführung des Webhook-Servers mit Uvicorn und lokale Tests
Jetzt testen wir kurz, ob der geschriebene FastAPI-Webhooks-Server lokal richtig funktioniert. Wechseln Sie im Verzeichnis ~/projects/webhook_server
mit aktiver virtueller Umgebung in die Eingabeaufforderung und führen Sie den folgenden Befehl aus.
# Überprüfen Sie, ob die virtuelle Umgebung aktiv ist (sollte (venv) vorangestellt sein)
# source venv/bin/activate # wenn nicht aktiv, führen Sie diesen Befehl erneut aus
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
Die Option
--reload
ist praktisch, da sie den Server automatisch neu startet, wenn der Code während der Entwicklung geändert wird.
Greifen Sie über 127.0.0.1:8000/docs im Browser zu und überprüfen Sie, ob die API-Dokumentation korrekt erstellt wurde. Sollte die Dokumentation erscheinen, läuft die Anwendung einwandfrei. Wenn Sie die IP-Adresse des Servers (z. B. 192.168.1.100
) kennen, können Sie von Ihrem lokalen Computer aus den folgenden curl
-Befehl verwenden, um einen Test-Request zu senden.
# Ändern Sie die IP-Adresse des Servers in 'YOUR_SERVER_IP'.
# Ersetzen Sie 'YOUR_WEBHOOK_SECRET' durch den tatsächlichen Wert der .env-Datei.
# Einschließlich einfacher Dummy-JSON-Daten.
curl -X POST http://YOUR_SERVER_IP:8000/webhook \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-H "X-Hub-Signature-256: sha256=$(echo -n '{"repository":{"name":"your-repo"},"ref":"refs/heads/main","pusher":{"name":"test-user"}}' | openssl dgst -sha256 -hmac 'YOUR_WEBHOOK_SECRET' | awk '{print $2}')" \
-d '{"repository":{"name":"your-repo"},"ref":"refs/heads/main","pusher":{"name":"test-user"}}'
Stellen Sie sicher, dass die Meldung Webhook empfangen und Verarbeitung im Hintergrund gestartet!
im Terminal angezeigt wird und auch im Serverprotokolldatei (webhook_server.log
) protokolliert wird. Sollte ein Fehler auftreten, können Sie die Fehlermeldungen im Terminal verwenden, um zu debuggen.
7. Fazit: Ausblick auf den nächsten Teil
In diesem Teil haben wir die Umgebung des Staging-Servers eingerichtet und die grundlegende Struktur des FastAPI-Webhooks-Servers entwickelt, der in der Lage ist, GitHub Webhook-Anfragen zu empfangen und das Secret zu überprüfen. Jetzt ist der Webhook-Server bereit, externe Anfragen sicher zu empfangen.
Im nächsten Teil werden wir auf den Grundlagen, die wir heute gelegt haben, die tatsächliche Deployment-Logik (Git Pull, Entscheidung zur Neubau von Docker-Images, Ausführung von Docker Compose) umsetzen und die Registrierung des FastAPI-Webhooks-Servers als Systemd-Dienst behandeln, um sicherzustellen, dass er auch beim Neustart des Servers automatisch ausgeführt wird. Bleiben Sie dran!
Es sind keine Kommentare vorhanden.