1. Вступление: время поработать с кодом
Здравствуйте! В первой части мы подробно обсудили причины создания автоматизированной системы развертывания с GitHub Webhook и необходимые подготовительные шаги, а во второй части рассмотрели общую архитектуру и ключевую роль сервиса вебхука FastAPI. Теперь пришло время перенести нашу идею на реальный код.
Если вы не смотрели предыдущие части, вы можете ознакомиться с ними по следующим ссылкам.
Создание моей автоматизированной системы развертывания с GitHub Webhook
1 - Зачем реализовывать это самостоятельно? 2 - Общая архитектура и проектирование процесса
В третьей части мы завершим базовую настройку среды на стадии и вместе создадим основу вебхука сервера FastAPI, который сможет безопасно принимать и проверять запросы GitHub Webhook. Этот процесс приближает нас на шаг к реальной автоматизированной системе развертывания!
2. Начальная настройка среды на стадии
Первое, что нужно сделать, это подготовить среду на стадии, где будет происходить развертывание, чтобы мы могли запускать код. Подключитесь к серверу по SSH и следуйте приведенным ниже шагам.
2.1. Подготовка среды разработки Python
Настройка виртуальной среды Python на сервере является хорошей практикой для предотвращения конфликтов зависимостей между проектами и поддержания чистой среды разработки. Установите необходимые пакеты с pip в виртуальную среду venv.
# Установка FastAPI, Uvicorn и python-dotenv для чтения .env файла
pip install fastapi uvicorn python-dotenv
2.2. Проверка установки Git, Docker, Docker Compose
Как уже упоминалось в предыдущей части, Git, Docker и Docker Compose являются основными инструментами для автоматизированного развертывания. Предполагается, что у читателей уже установлены эти инструменты на сервере.
Если они не установлены, сначала проверьте их наличие с помощью следующих команд.
git --version
docker --version
docker compose version # или docker-compose --version (в зависимости от способа установки)
Если они не установлены, пожалуйста, следуйте инструкциям в официальной документации для установки каждого инструмента. Если вам нужно более подробное руководство по установке Docker, вы можете ознакомиться с этой статьей: Руководство по установке Docker
Примечание: После установки Docker необходимо предоставить группе пользователю права доступа и выйти из системы, а затем войти снова, чтобы использовать команды docker
без sudo
.
sudo usermod -aG docker $USER
После предоставления прав Docker обязательно выйдите из системы на сервере и войдите снова, чтобы изменения вступили в силу. Проверьте работу с помощью команды docker run hello-world
без sudo
.
3. Создание базовой структуры вебхука сервера FastAPI
Теперь создадим базовый каркас приложения FastAPI, которое будет принимать вебхуки. В каталоге ~/projects/webhook_server
создайте файл main.py
и вставьте следующий код.
3.1. Создание файла main.py
и инициализация приложения FastAPI
Я собрал всю логику в файле main.py
для объяснения, но вам стоит разбить логику на отдельные модули и импортировать их для удобства в вашем реальном проекте. Вот пример кода.
# ~/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 (для использования переменных окружения)
load_dotenv()
# Настройка логирования
logging.basicConfig(
level=logging.INFO, # Запись логов уровнем INFO и выше
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("webhook_server.log"), # Сохранение логов в файл
logging.StreamHandler() # Вывод логов в консоль
]
)
logger = logging.getLogger(__name__)
app = FastAPI()
# Загрузка секрета вебхука GitHub из переменных окружения
GITHUB_WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET")
# Тестовая корневая конечная точка: в производстве рекомендуется удалить ее для безопасности.
@app.get("/")
async def read_root():
return {"message": "Сервер вебхука работает!"}
# Конечная точка для получения вебхука от GitHub
@app.post("/webhook")
async def github_webhook(request: Request, background_tasks: BackgroundTasks):
logger.info("Запрос вебхука получен.")
# 1. Проверка секрета вебхука GitHub (это должен быть первый шаг безопасности)
if GITHUB_WEBHOOK_SECRET:
signature = request.headers.get("X-Hub-Signature-256")
if not signature:
logger.warning("Отсутствует заголовок подписи. Операция отменена.")
raise HTTPException(status_code=401, detail="Отсутствует заголовок X-Hub-Signature-256")
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("Недопустимая подпись. Операция отменена.")
raise HTTPException(status_code=401, detail="Недопустимая подпись")
else:
logger.info("Подпись успешно проверена.")
else:
logger.warning("GITHUB_WEBHOOK_SECRET не установлен. Пропущена проверка подписи (НЕ РЕКОМЕНДУЕТСЯ ДЛЯ ПРОИЗВОДСТВА).")
# 2. Парсинг тела HTTP (Payload)
try:
payload = await request.json()
event_type = request.headers.get("X-GitHub-Event")
# Извлечение имени репозитория из полезной нагрузки вебхука GitHub.
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"Событие: {event_type}, Репозиторий: {repo_name}, Референс: {ref}, Пушер: {pusher_name}")
# Фоновый запуск логики развертывания
background_tasks.add_task(handle_deploy, repo_name)
except Exception as e:
logger.error(f"Ошибка парсинга полезной нагрузки вебхука или обработки запроса: {e}")
raise HTTPException(status_code=400, detail=f"Ошибка обработки запроса: {e}")
# Немедленный ответ GitHub
return {"message": "Вебхук получен, обработка начата в фоне!"}
# Функция для обработки логики развертывания
def handle_deploy(repo_name: str):
if repo_name == "deployer":
logger.info("⚙️ Пропущено самообновление развёртывателя.")
return
# Чтение локального пути каждого репозитория из переменных окружения.
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"),
}
# Проверка на наличие изменений.
repo_path: Optional[str] = repo_paths.get(repo_name)
if not repo_path:
logger.warning(f"🚫 Неизвестный репозиторий: {repo_name}")
return
repo_path = Path(repo_path)
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"
deploy_color = env_vars.get("COLOR", "red")
project_name = str(deploy_color) + p_name.get(repo_name, repo_name)
docker_compose_file = "docker-compose.dev.yml" if is_debug else "docker-compose.prod.yml"
compose_file = repo_path / docker_compose_file
logger.info(f"Извлечение последнего кода для {repo_name} в {repo_path}...")
try:
subprocess.run(["git", "-C", repo_path, "pull"], check=True, capture_output=True, text=True)
logger.info(f"Git pull успешен для {repo_name}.")
except subprocess.CalledProcessError as e:
logger.error(f"Git pull не удался для {repo_name}: {e.stderr}")
return
logger.info(f"Проверка изменений в {repo_name} для принятия решения о повторной сборке...")
changed = should_rebuild(repo_path)
try:
if changed:
logger.info(f"🚀 Обнаружены изменения. Сборка и развертывание {repo_name} с помощью {docker_compose_file}...")
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 сборка и развертывание успешны для {repo_name}.")
else:
logger.info(f"✨ Нет значительных изменений. Развёртывание {repo_name} без повторной сборки изображений с использованием {docker_compose_file}...")
subprocess.run(["docker", "compose", "-p", project_name, "--no-deps", "-f", compose_file, "up", "-d"], check=True, capture_output=True, text=True)
logger.info(f"Docker compose успешен для {repo_name}.")
except subprocess.CalledProcessError as e:
logger.error(f"Docker compose не удался для {repo_name}: {e.stderr}")
return
logger.info(f"✅ Задача развертывания для {repo_name} завершена.")
# Функция для определения необходимости повторной сборки изображения Docker
def should_rebuild(repo_path: Path) -> bool:
trigger_files = [
"requirements.txt", "Dockerfile", "Dockerfile.dev", "Dockerfile.prod",
".dockerignore", ".env"
]
logger.info(f"Проверка git diff для {repo_path}...")
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"Измененные файлы в git: {changed_files_in_git}")
except subprocess.CalledProcessError as e:
logger.error(f"Git diff не удался для {repo_path}: {e.stderr}")
return True
if any(f in changed_files_in_git for f in trigger_files):
logger.info("Обнаружены изменения триггерных файлов через git diff. Повторная сборка необходима.")
return True
rebuild_trigger_file = repo_path / "REBUILD_TRIGGER"
if rebuild_trigger_file.exists():
try:
rebuild_trigger_file.unlink()
logger.info("Обнаружен триггерный файл REBUILD_TRIGGER. Повторная сборка обязательна, файл удалён.")
return True
except Exception as e:
logger.error(f"Ошибка при удалении файла REBUILD_TRIGGER: {e}")
return True
logger.info("Триггерные файлы для повторной сборки не найдены. Повторная сборка не требуется.")
return False
3.2. Определение конечной точки /webhook
В коде, под декоратором app.post("/webhook")
, определена асинхронная функция github_webhook
, которая и является конечной точкой для получения запросов от GitHub Webhook. Эта функция обрабатывает входящие POST
запросы и позволяет получить доступ к заголовкам и содержимому (Payload) через объект Request
. BackgroundTasks
в FastAPI позволяет выполнять длительные задачи, такие как развертывание, в фоновом режиме, при этом отправляя немедленный ответ GitHub.
4. Реализация логики проверки GitHub Webhook Secret
Наиболее важным элементом безопасности в приведённом коде является логика проверки с использованием GITHUB_WEBHOOK_SECRET
.
4.1. Важность значения Secret
При настройке вебхука GitHub вы можете задать Secret
значение. Это значение используется GitHub для генерации хеш-значения, комбинируя тело запроса и значение секрета, и отправляет его в заголовке X-Hub-Signature-256
. Наш сервер вебхука создает хеш-значение тем же способом и сравнивает его с хеш-значением, отправленным GitHub, чтобы убедиться, что запрос пришел точно от GitHub и что данные не были подменены при передаче.
4.2. Проверка с использованием заголовка X-Hub-Signature-256
Как вы видели в коде, мы используем модули hmac
и hashlib
для выполнения проверки секрета.
- Настройка секрета: Сначала создайте файл
.env
в каталоге~/projects/webhook_server
и запишите туда значение секрета, которое будете использовать для настройки вебхука GitHub. Этот файл следует добавить в.gitignore
, чтобы он не попал в Git-репозиторий. Это значение секрета будет использоваться в будущем в настройках вебхуков репозитория GitHub.
# ~/projects/webhook_server/.env
GITHUB_WEBHOOK_SECRET="Введите_значение_секрета_для_настройки_GitHub_вебхука_здесь"
- Код FastAPI: В
main.py
мы используем библиотекуpython-dotenv
для загрузки этого файла.env
, и с помощьюos.getenv()
получаем значениеGITHUB_WEBHOOK_SECRET
. Каждый раз, когда поступает запрос вебхука, мы извлекаем значение подписи черезRequest.headers.get("X-Hub-Signature-256")
и используемhmac.new()
для вычисления ожидаемой подписи для сравнения.
Эта логика проверки является неотъемлемой защитой от внешних атак на конечную точку вебхука, поэтому ни в коем случае не следует её пропускать!
5. Настройка системы логирования
В коде используется модуль logging
для базовой настройки логирования.
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("webhook_server.log"), # Сохранение в файл webhook_server.log
logging.StreamHandler() # Вывод в консоль
]
)
logger = logging.getLogger(__name__)
С такой настройкой каждая вебхука, поступающая на сервер, будет записываться в файл webhook_server.log
, и одновременно выводиться в терминал. Позже, когда логика развертывания станет сложнее, через логирование будет легко определить, на каком этапе возникла проблема.
6. Запуск вебхука сервера с помощью Uvicorn и локальное тестирование
Теперь давайте кратко протестируем, правильно ли работает созданный нами сервер вебхука FastAPI на локальном сервере. В каталоге ~/projects/webhook_server
на сервере стадии активируйте виртуальную среду и выполните следующую команду.
# Убедитесь, что виртуальная среда активирована (проверьте, имеется ли (venv) в начале строки)
# source venv/bin/activate # Запустите эту команду, если она не была активирована
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
Опция `--reload` автоматически перезапускает сервер при изменении кода, что удобно в процессе разработки.
Перейдите в браузере по адресу 127.0.0.1:8000/docs, чтобы проверить, корректно ли сгенерирована документация API. Если документация отображается, значит приложение работает нормально. Если вы знаете IP-адрес сервера (например, 192.168.1.100
), вы можете отправить тестовый запрос с помощью следующей команды curl
с локального устройства.
```bash
Замените IP-адрес сервера на 'YOUR_SERVER_IP'.
'YOUR_WEBHOOK_SECRET' нужно заменить на реальное значение из файла .env.
Включает простые тестовые JSON-данные.
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"}}'
```
Убедитесь, что в терминале выводится сообщение Webhook received and processing started in background!
и оно также записывается в файл логов сервера (webhook_server.log
). Если возникают ошибки, вы можете отладить их, изучая сообщения об ошибках, выводимые в терминале.
7. Заключение: анонс следующей части
В этой части мы построили среду на стадии и завершили создание базовой структуры FastAPI вебхука сервера, который может безопасно принимать запросы извне.
В следующей части мы будем использовать созданную основу для реализации логики развертывания (Git Pull, определение, требуется ли повторная сборка Docker-изображения, выполнение Docker Compose) с использованием модуля subprocess
, а также подробно расскажем о том, как зарегистрировать этот сервер вебхука FastAPI как сервис Systemd, чтобы он автоматически запускался при перезагрузке сервера. Ожидайте с нетерпением!
Комментариев нет.