From 82cbae7ae21469e32f896f66a78fe31d78cf1fb1 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 25 Jan 2026 17:28:58 +0900 Subject: [PATCH] =?UTF-8?q?webhook=20=EC=84=A4=EC=A0=95=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20=20-=20deployer=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20webhook=20=EC=98=A4=EB=A5=98=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deployer/Dockerfile | 13 +++++++++++- deployer/app.py | 20 ++++++++++++------ deployer/requirements.txt | 2 ++ docker-compose.yml | 3 ++- nginx/default.conf | 21 +++++++++++++++++++ scripts/deploy-nas.sh | 43 +++++++++++++++++++++++++-------------- scripts/deploy.sh | 4 +++- 7 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 deployer/requirements.txt diff --git a/deployer/Dockerfile b/deployer/Dockerfile index 79ca7f5..dad993c 100644 --- a/deployer/Dockerfile +++ b/deployer/Dockerfile @@ -1,5 +1,16 @@ FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git rsync ca-certificates curl \ + docker.io \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app -RUN pip install --no-cache-dir fastapi uvicorn +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + COPY app.py /app/app.py + +ENV PYTHONUNBUFFERED=1 +EXPOSE 9000 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "9000"] diff --git a/deployer/app.py b/deployer/app.py index 2d72f39..9c6cb6b 100644 --- a/deployer/app.py +++ b/deployer/app.py @@ -5,25 +5,33 @@ app = FastAPI() SECRET = os.getenv("WEBHOOK_SECRET", "") def verify(sig: str, body: bytes) -> bool: - # Gitea: X-Gitea-Signature = sha256=... - if not SECRET: + if not SECRET or not sig: return False + mac = hmac.new(SECRET.encode(), msg=body, digestmod=hashlib.sha256).hexdigest() - expected = f"sha256={mac}" - return hmac.compare_digest(expected, sig) + + # Gitea가 보내는 포맷이 케이스별로 달라서 둘 다 허용 + candidates = {mac, f"sha256={mac}"} + return any(hmac.compare_digest(sig, c) for c in candidates) @app.post("/webhook") async def webhook(req: Request): body = await req.body() - sig = req.headers.get("X-Gitea-Signature", "") + + # ✅ 여기(함수 안)에서 헤더 읽기 + sig = ( + req.headers.get("X-Gitea-Signature") + or req.headers.get("X-Hub-Signature-256") + or "" + ) if not verify(sig, body): raise HTTPException(401, "bad signature") # 배포 스크립트 실행 - # (컨테이너에 /scripts 가 마운트되어 있어야 함) p = subprocess.run(["/scripts/deploy.sh"], capture_output=True, text=True) if p.returncode != 0: raise HTTPException(500, f"deploy failed:\n{p.stdout}\n{p.stderr}") return {"ok": True, "out": p.stdout} + diff --git a/deployer/requirements.txt b/deployer/requirements.txt new file mode 100644 index 0000000..97dc7cd --- /dev/null +++ b/deployer/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn diff --git a/docker-compose.yml b/docker-compose.yml index 6729189..6391834 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,8 +56,9 @@ services: ports: - "19010:9000" # 외부 노출 필요 없으면 내부만 (리버스프록시로 /webhook만 노출 추천) environment: - - WEBHOOK_SECRET=여기에_긴_랜덤값 + - WEBHOOK_SECRET=webpage_deploy_7f3A9cE2KpLwQ8N5VxRZbD4mH6TYeJ volumes: - /volume1/workspace/web-page-backend:/repo:rw - /volume1/docker/webpage:/runtime:rw - /volume1/docker/webpage/scripts:/scripts:ro + - /var/run/docker.sock:/var/run/docker.sock diff --git a/nginx/default.conf b/nginx/default.conf index aa4c156..2cf0ed9 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -66,6 +66,27 @@ server { proxy_pass http://backend:8000; } + # webhook receiver (handle both /webhook and /webhook/) + location = /webhook { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://deployer:9000/webhook; + } + + location /webhook/ { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://deployer:9000/webhook; + } + # SPA 라우팅 (마지막에 두는 게 안전) location / { try_files $uri $uri/ /index.html; diff --git a/scripts/deploy-nas.sh b/scripts/deploy-nas.sh index 1cce635..279f21d 100644 --- a/scripts/deploy-nas.sh +++ b/scripts/deploy-nas.sh @@ -1,21 +1,34 @@ -#!/usr/bin/env bash +#!/bin/bash set -euo pipefail -ROOT="/volume1/docker/webpage" +SRC="/repo" +DST="/runtime" # (= /volume1/docker/webpage 가 마운트된 곳) -cd "$ROOT" +cd "$SRC" -echo "[1/5] git fetch + pull" -git fetch --all --prune -git pull --ff-only +# 레포에서 운영으로 반영할 항목들만 복사/동기화 (필요한 것만 적기) +# backend, travel-proxy, deployer, nginx, scripts, docker-compose.yml, .env 등 +rsync -a --delete \ + --exclude ".git" \ + --exclude ".releases" \ + "$SRC/backend/" "$DST/backend/" +rsync -a --delete \ + --exclude ".git" \ + "$SRC/travel-proxy/" "$DST/travel-proxy/" +rsync -a --delete \ + --exclude ".git" \ + "$SRC/deployer/" "$DST/deployer/" +rsync -a --delete \ + --exclude ".git" \ + "$SRC/nginx/" "$DST/nginx/" +rsync -a --delete \ + --exclude ".git" \ + "$SRC/scripts/" "$DST/scripts/" -echo "[2/5] docker compose build" -docker compose build --pull +# compose 파일 / env 파일 +rsync -a "$SRC/docker-compose.yml" "$DST/docker-compose.yml" +if [ -f "$SRC/.env" ]; then + rsync -a "$SRC/.env" "$DST/.env" +fi -echo "[3/5] docker compose up" -docker compose up -d - -echo "[4/5] status" -docker compose ps - -echo "[5/5] done" +echo "SYNC_OK" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index ab7f82b..3ce88b8 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -4,6 +4,8 @@ set -euo pipefail SRC="/repo" DST="/runtime" +git config --global --add safe.directory "$SRC" + cd "$SRC" git fetch --all --prune git pull --ff-only @@ -20,5 +22,5 @@ rsync -a --delete \ bash "$SRC/scripts/deploy-nas.sh" cd "$DST" -docker compose up -d --build backend travel-proxy frontend +docker-compose up -d --build backend travel-proxy frontend echo "DEPLOY_OK $TAG"