Deployment — Docker, CI/CD, Monitoring
Deployment — Docker, CI/CD, Monitoring
Перехід від розробки додатка на локальній машині до його запуску у реальному production-середовищі — це крок, який вимагає зміни мислення розробника. Локально ми цінуємо швидкість автоперезапуску коду (--reload), детальне відображення помилок на екрані та мінімалістичні in-memory бази даних. У production на перше місце виходять зовсім інші вимоги: надійність, безпека, масштабованість, швидкість завантаження та повний моніторинг стану системи.
Для .NET розробників концепції публікації застосунків є звичними завдяки потужним вбудованим інструментам SDK та IIS/Kestrel. В екосистемі Python процес розгортання є більш гнучким, але водночас вимагає від розробника самостійного збору «конструктора» з різних утиліт: контейнеризація, менеджери процесів, брокери та системи моніторингу.
У цій статті ми пройдемо повний шлях розгортання сучасного FastAPI додатка: від правильної валідації конфігурації оточення до збірки оптимізованих Docker-образів, налаштування CI/CD та підключення систем збору логів та метрик.
1. Конфігурація середовища: Pydantic Settings та 12-Factor App
Перше правило безпечного деплою — ніколи не зберігайте паролі, секретні ключі та налаштування баз даних у коді додатку. Це порушує один із ключових принципів методології розробки хмарних додатків 12-Factor App (розділ III. Config), який стверджує: Конфігурація має зберігатися в змінних оточення (environment variables).
У Python стандартним і найбільш надійним інструментом для роботи з конфігурацією є бібліотека Pydantic Settings (про яку ми детально говорили у Статті 15).
Встановлення залежностей
Для роботи з конфігурацією встановимо необхідні бібліотеки:
pip install pydantic-settings python-dotenv
uv add pydantic-settings python-dotenv
poetry add pydantic-settings python-dotenv
Практична реалізація конфігурації
Створимо клас конфігурації, який автоматично зчитуватиме змінні оточення, валідуватиме їхні типи та матиме локальний фолбек (fallback) у вигляді файлу .env для розробки.
from typing import Literal
from pydantic import PostgresDsn, RedisDsn, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# Визначаємо рівень запуску додатка
ENVIRONMENT: Literal["development", "staging", "production"] = "development"
# Секретний ключ для шифрування JWT токенів
SECRET_KEY: str = Field(min_length=32)
# Валідація форматів підключення до БД та Redis через спеціальні типи Pydantic
DATABASE_URL: PostgresDsn
REDIS_URL: RedisDsn
# Порт для запуску API
PORT: int = 8000
# Налаштування джерела зчитування
# env_file вказує на файл, який зчитується локально, якщо системні змінні відсутні
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True # Змінні в коді та в системі мають збігатися за регістром
)
# Створюємо єдиний екземпляр налаштувань (Singleton)
settings = Settings()
Створимо локальний файл конфігурації .env (його обов'язково треба додати до .gitignore!):
ENVIRONMENT=development
SECRET_KEY=super-secret-key-that-must-be-at-least-32-chars-long
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/taskforge
REDIS_URL=redis://localhost:6379/0
PORT=8000
Порівняльний аналіз конфігурації: .NET ↔ Python
Для розробників, які звикли до appsettings.json, підхід у Python може здатися занадто суворим через валідацію типів. Давайте порівняємо ці моделі:
| Параметр порівняння | .NET Configuration (IConfiguration) | Python Pydantic Settings |
|---|---|---|
| Формат файлів | JSON (appsettings.json, appsettings.Development.json) | Системні env змінні або текстові .env файли |
| Валідація типів | Відсутня за замовчуванням (зчитує все як рядки. Потрібно писати кастомні перевірки чи біндінг в Options) | Вбудована та обов'язкова (якщо передати рядок у поле PORT: int, виникне помилка і додаток не запуститься) |
| Помилки конфігурації | Виникають під час виконання (Runtime), якщо код спробує зчитати пусте значення | Виникають на етапі запуску (Startup). Додаток впаде миттєво, якщо хоч одна змінна не пройде валідацію |
| Ієрархія перевизначення | Зчитує JSON → додає User Secrets → додає Environment Variables | Системні змінні оточення мають абсолютний пріоритет над локальним .env файлом |
DATABASE_URL у Kubernetes Deployment, контейнер просто впаде під час старту, запобігаючи деплою некоректної конфігурації.2. Вебсервери в Production: Uvicorn та Gunicorn
Локально ми запускаємо FastAPI однією командою: uvicorn main:app --reload. Прапорець --reload запускає додатковий процес, який постійно моніторить зміни файлів на диску. Це створює велике навантаження на процесор та споживає зайву пам'ять.
--reload у production!
Окрім оверхеду на ресурси, автоперезапуск коду в production є критичною дірою в безпеці — якщо зловмисник зможе завантажити або змінити будь-який файл на сервері, вебсервер автоматично перезавантажиться і виконає шкідливий код.Оптимальний запуск Uvicorn
У production ми маємо запускати Uvicorn без reload та з обмеженням кількості процесів (workers). Оскільки Python виконує код в одному потоці через GIL, щоб задіяти всі ядра процесора сервера, нам потрібно запустити кілька копій (воркерів) нашого додатка.
Формула розрахунку оптимальної кількості воркерів:
Запуск Uvicorn з кількома воркерами:
Використання Gunicorn як менеджера процесів
Хоча Uvicorn має свій простий менеджер воркерів (--workers), для великих систем стандартом є запуск Uvicorn під керуванням Gunicorn (Green Unicorn).
Gunicorn — це зрілий WSGI/ASGI менеджер процесів, який:
- Моніторить стан воркерів. Якщо один із воркерів впаде через витік пам'яті або критичну помилку (Segfault), Gunicorn миттєво та непомітно для користувачів запустить новий.
- Дозволяє робити "Graceful reload" без зупинки сервера (за допомогою системного сигналу
SIGHUP). - Має кращі алгоритми розподілу запитів між воркерами.
pip install gunicorn
uv add gunicorn
poetry add gunicorn
Запуск додатка за допомогою Gunicorn та Uvicorn-воркерів:
Graceful Shutdown (М'яка зупинка)
У хмарних середовищах (наприклад, Kubernetes або AWS ECS) контейнери постійно зупиняються та запускаються знову при масштабуванні чи оновленні. Коли сервер отримує команду на зупинку (SIGTERM), він має завершити її коректно:
- Припинити приймати нові HTTP-запити.
- Дати активним запитам завершити обробку (наприклад, закінчити запис у базу даних).
- Закрити підключення до БД та Redis.
- Завершити процес.
Gunicorn та Uvicorn роблять це автоматично. За замовчуванням час очікування (graceful timeout) становить 30 секунд. Якщо запити не завершаться за цей час, процес буде вбито примусово через SIGKILL.
Порівняльний аналіз хостингу: Kestrel ↔ Uvicorn
| Критерій | .NET Kestrel | Python Uvicorn / Gunicorn |
|---|---|---|
| Архітектура | Повністю готовий до production вебсервер. Може безпечно дивитися напряму в інтернет (edge server) | ASGI-сервер. Зазвичай потребує зворотної проксі (Nginx/Traefik) перед собою для SSL та захисту від DDoS |
| Багатопотоковість | Вбудований асинхронний Thread Pool на рівні CLR (один процес обробляє всі запити паралельно) | Процесна модель ( GIL змушує запускати окремий процес (worker) на кожне ядро CPU) |
| Менеджер процесів | Не потрібен. Процесом керує рантайм .NET та системні засоби (systemd, IIS) | Потрібен додатковий менеджер (Gunicorn) для відстеження стану процесів-воркерів |
3. Контейнеризація за допомогою Docker
Сьогодні стандартом розгортання вебзастосунків є контейнеризація. Docker гарантує, що додаток працюватиме в хмарі (Production) точно так само, як і на локальному комп'ютері розробника (Development), оскільки він упаковує код разом з усіма залежностями, системними бібліотеками та конфігураціями в ізольоване середовище.
У розділі 07.tools/01.docker/ ви вже вивчили базові концепції Docker. Тепер ми розберемо, як створити безпечний та оптимізований для продакшену Docker-образ для FastAPI додатку.
Вибір базового образу: Slim vs Alpine
Перше рішення при написанні Dockerfile — вибір базового образу (Base Image). В екосистемі Python є два основних варіанти мінімалістичних образів:
python:3.12-alpine(на базі Alpine Linux):- Має надзвичайно малий розмір (близько 50 МБ).
- Проблема з компіляцією C-extensions: Alpine використовує бібліотеку
musl libcзамість стандартноїglibc(яка є в Debian/Ubuntu). Багато популярних Python бібліотек (наприклад,bcrypt,cryptography,psycopg2для PostgreSQL,numpy,pandas) містять бінарний C-код. Для стандартних Linux-систем розробники випускають готові скомпільовані пакети (wheels). Але для Alpine готових wheels часто немає — Docker почне компілювати цей C-код із сирців прямо під час збірки образу. Це вимагає встановлення важких інструментів компіляції (gcc,make,g++), що сильно розтягує час збірки (з 1 хвилини до 15) та збільшує розмір фінального образу, нівелюючи всю перевагу Alpine.
python:3.12-slim(на базі Debian):- Розмір трохи більший (близько 120 МБ).
- Використовує стандартну
glibc, тому всі готові скомпільовані колеса (wheels) встановлюються миттєво без компіляції. Це найкращий вибір для більшості Python вебдодатків у production.
Multi-stage Builds: Оптимізація та безпека
Для збірки образу ми використаємо патерн Multi-stage build (Багатоетапна збірка). Ми розділимо процес на два кроки:
- Stage 1 (builder): Встановлюємо важкі інструменти розробки (наприклад, менеджер залежностей
poetryабоuv), компілюємо необхідні бібліотеки та збираємо віртуальне середовище Python. - Stage 2 (runner): Беремо чистий базовий образ
slim, копіюємо туди лише готове віртуальне середовище з першого етапу та код нашого додатка. Усі інструменти збірки (компілятори,poetryфайли) залишаються в першому етапі й не потрапляють у продакшн.
Це дозволяє зменшити розмір образу в 2-3 рази та прибрати зайві вразливості безпеки (наприклад, якщо у вашому контейнері немає компиляторів та утиліт розробника, зловмиснику буде значно важче закріпитися в системі при зламі).
Написання Dockerfile
Створимо оптимізований Dockerfile з урахуванням layer caching (кешування шарів) та запуском процесу від імені безпечного не-root користувача. Виберіть варіант залежно від вашого менеджера пакетів:
# ==========================================
# STAGE 1: Builder (Етап збірки залежностей)
# ==========================================
FROM python:3.12-slim AS builder
# Вимикаємо створення файлів кену (.pyc) та вмикаємо буферизацію логів
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /build
# Встановлюємо інструменти компіляції, якщо вони потрібні для встановлення деяких пакетів
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Встановлюємо менеджер пакетів (наприклад, pip + wheel)
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
# Копіюємо лише файли залежностей (layer caching)
# Якщо залежності не змінювалися, цей крок буде зчитано з кешу, що прискорить збірку
COPY requirements.txt .
# Встановлюємо залежності у локальну папку (віртуальне середовище)
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ==========================================
# STAGE 2: Runner (Етап запуску додатку)
# ==========================================
FROM python:3.12-slim AS runner
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Копіюємо скомпільовані бібліотеки з builder-етапу
COPY --from=builder /install /usr/local
# Безпека: Створюємо не-привілейованого користувача системи (non-root user)
# За замовчуванням Docker запускає процеси від імені root, що є критичною вразливістю
RUN groupadd -g 10001 appuser && \
useradd -u 10000 -g appuser -m -s /sbin/nologin appuser
# Копіюємо вихідний код додатку
COPY ./app ./app
COPY ./main.py .
# Передаємо права на папку нашому користувачу
RUN chown -R appuser:appuser /app
# Перемикаємось на безпечного користувача
USER appuser
# Відкриваємо порт
EXPOSE 8000
# Запускаємо через Gunicorn
CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
# ==========================================
# STAGE 1: Builder (Збірка через uv)
# ==========================================
FROM python:3.12-slim AS builder
# Копіюємо утиліту uv з офіційного образу
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /build
# Налаштування компіляції байткоду для uv та копіювання файлів
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
# Копіюємо опис залежностей
COPY pyproject.toml uv.lock ./
# Синхронізуємо залежності без локального проекту (створюється .venv)
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev --no-editable
# ==========================================
# STAGE 2: Runner
# ==========================================
FROM python:3.12-slim AS runner
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Копіюємо створене віртуальне оточення (.venv)
COPY --from=builder /build/.venv /app/.venv
# Додаємо віртуальне оточення до системного шляху
ENV PATH="/app/.venv/bin:$PATH"
# Безпека: Створюємо не-привілейованого користувача системи
RUN groupadd -g 10001 appuser && \
useradd -u 10000 -g appuser -m -s /sbin/nologin appuser
# Копіюємо вихідний код додатку
COPY ./app ./app
COPY ./main.py .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
# ==========================================
# STAGE 1: Builder (Збірка через Poetry)
# ==========================================
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Змінні середовища для налаштування Poetry
ENV POETRY_VERSION=1.8.3
ENV POETRY_HOME="/opt/poetry"
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
ENV PATH="$POETRY_HOME/bin:$PATH"
WORKDIR /build
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Встановлюємо Poetry через офіційний скрипт
RUN curl -sSL https://install.python-poetry.org | python3 -
# Копіюємо опис залежностей
COPY pyproject.toml poetry.lock ./
# Встановлюємо лише залежності для production (без самого проекту)
RUN poetry install --no-root --only main
# ==========================================
# STAGE 2: Runner
# ==========================================
FROM python:3.12-slim AS runner
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Копіюємо створене віртуальне оточення (.venv) з builder
COPY --from=builder /build/.venv /app/.venv
# Додаємо віртуальне оточення до системного шляху
ENV PATH="/app/.venv/bin:$PATH"
# Безпека: Створюємо не-привілейованого користувача системи
RUN groupadd -g 10001 appuser && \
useradd -u 10000 -g appuser -m -s /sbin/nologin appuser
# Копіюємо вихідний код додатку
COPY ./app ./app
COPY ./main.py .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
Також створимо файл .dockerignore в корені проекту, щоб не копіювати непотрібні локальні файли в образ (наприклад, локальну базу даних чи файли Git):
.git
.github
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.env
*.db
tests/
Dockerfile
docker-compose.yml
Порівняльний аналіз: Dockerfile у .NET ↔ Python
Для розробників із .NET структура Dockerfile з розділенням на SDK (builder) та Runtime (runner) є рідною. Зіставимо їхні етапи:
# Builder етап використовує важкий SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app
# Runner етап використовує легкий Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app .
USER $APP_UID
ENTRYPOINT ["dotnet", "MyApp.dll"]
Ключові паралелі:
- SDK vs Python Builder: В .NET ми використовуємо образ
dotnet/sdkдля відновлення пакетів (dotnet restore) та компіляції. У Python ми використовуємо образpython-slimі за потреби встановлюємо системні утиліти (build-essential) для компіляції C-залежностей. - Runtime vs Python Runner: В .NET фінальний образ базується на
aspnet(де є тільки CLR runtime). У Python ми копіюємо вміст папки/install(всі встановлені pip бібліотеки) у системні папки чистішого образуpython-slim. - Non-root user: В .NET 8 за замовчуванням налаштована змінна
USER $APP_UID(безпечний користувачappз ID 1654). У Python ми маємо явно прописатиRUN useradd ...таUSER appuserу Dockerfile, якщо не використовуємо спеціально налаштовані сторонні образи.
4. Оркестрація локального середовища: Docker Compose
Сучасні додатки майже ніколи не працюють самі по собі. Нашому FastAPI потрібні:
- Реляційна база даних (PostgreSQL).
- Сховище для кешу та черг (Redis).
Для швидкого запуску та оркестрації всієї цієї інфраструктури однією командою використовують Docker Compose.
Зв'язок контейнерів та проблема готовності (Health Checks)
Одна з найпоширеніших помилок при написанні docker-compose.yml — нерозуміння різниці між станом контейнера "Running" та реальною готовністю сервісу всередині нього приймати підключення.
depends_on: [db] для FastAPI, Docker Compose запустить контейнер з базою даних і відразу почне запускати контейнер з FastAPI. Проте PostgreSQL потребує близько 3-5 секунд на ініціалізацію внутрішніх файлів та запуск сокету. Як наслідок: FastAPI спробує підключитися до бази даних, отримає помилку ConnectionRefusedError і впаде з критичною помилкою на старті.Для вирішення цієї проблеми ми маємо налаштувати Health Check (перевірку здоров'я) для бази даних, а для FastAPI вказати запуск лише після того, як база даних пройде цю перевірку успішно.
Написання docker-compose.yml
Створимо файл конфігурації для локального запуску всієї системи:
version: '3.8'
services:
# 1. Сервіс Бази Даних PostgreSQL
db:
image: postgres:16-alpine
container_name: taskforge_postgres
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: super-secure-db-password
POSTGRES_DB: taskforge
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
# Налаштовуємо перевірку працездатності БД
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres -d taskforge']
interval: 5s # Як часто перевіряти
timeout: 5s # Таймаут на відповідь
retries: 5 # Скільки спроб до позначки "Unhealthy"
# 2. Сервіс Redis (Кеш та Черга задач)
redis:
image: redis:7-alpine
container_name: taskforge_redis
restart: always
ports:
- '6379:6379'
# Перевірка працездатності Redis
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 5
# 3. Наш FastAPI веб-додаток
web:
build:
context: .
dockerfile: Dockerfile
container_name: taskforge_api
restart: always
ports:
- '8000:8000'
environment:
- ENVIRONMENT=production
- SECRET_KEY=another-super-secret-key-for-prod-32-chars
# Зверніть увагу: ми використовуємо імена сервісів (db, redis) замість localhost!
# Docker Compose створює внутрішню мережу, де імена сервісів працюють як DNS-імена
- DATABASE_URL=postgresql://postgres:super-secure-db-password@db:5432/taskforge
- REDIS_URL=redis://redis:6379/0
# Чекаємо повної готовності залежних контейнерів
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
postgres_data: # Зберігає дані БД на хост-машині, щоб вони не зникли при видаленні контейнерів
Запустити весь проект можна однією командою у терміналі:
5. CI/CD автоматизація: GitHub Actions
Для того, щоб переконатися, що наш код відповідає стандартам якості (пройшов лінтинг, перевірку типів та всі unit-тести), а також автоматично збирати свіжі Docker-образи при кожному пуші в репозиторій, ми налаштуємо конвеєр автоматизації GitHub Actions.
Створимо файл конфігурації workflow у спеціальній папці .github/workflows/:
name: CI/CD Pipeline
# Тригери для запуску workflow
on:
push:
branches: ['main']
pull_request:
branches: ['main']
jobs:
# ----------------------------------------------------
# ЕТАП 1: Тестування та перевірка якості коду (Lint & Test)
# ----------------------------------------------------
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository code
uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
# Кешуємо залежності pip, щоб пришвидшити наступні збірки
cache: 'pip'
- name: Install dependencies
run: |
python -m pip upgrade pip
pip install -r requirements.txt
# Додаткові утиліти для тестування
pip install ruff mypy pytest pytest-asyncio httpx
- name: Run Ruff Linter
run: ruff check app/
- name: Run Ruff Formatter check
run: ruff format --check app/
- name: Run Mypy Type Checker
run: mypy app/
- name: Run Pytest suite
run: pytest tests/
env:
# Передаємо тестові налаштування
SECRET_KEY: test-secret-key-placeholder-32-chars-long
DATABASE_URL: sqlite+aiosqlite:///./test.db
REDIS_URL: redis://localhost:6379/0
# ----------------------------------------------------
# ЕТАП 2: Збірка Docker образу (Build & Push)
# Виконується лише якщо попередній етап пройшов успішно та гілка - main
# ----------------------------------------------------
build-and-publish:
needs: lint-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: actions/setup-buildx-action@v3
# Авторизуємось у GitHub Container Registry (GHCR)
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Збираємо образ та відправляємо його в реєстр
- name: Build and Push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
# Формуємо теги для нашого образу
tags: |
ghcr.io/${{ github.repository }}/api:latest
ghcr.io/${{ github.repository }}/api:${{ github.sha }}
# Кешування шарів Docker для прискорення збірок у хмарі
cache-from: type=gha
cache-to: type=gha,mode=max
6. Логування та Моніторинг у Production
Запустити додаток в Docker — це лише половина справи. Після запуску нам необхідно бачити, що відбувається всередині системи: чи є помилки, яка затримка запитів (latency) та чи не споживає додаток занадто багато пам'яті.
Структуроване логування з structlog
Стандартний модуль logging в Python за замовчуванням виводить текстові рядки. У великих системах, де логи збираються у централізовані сховища (Elasticsearch, Loki, Datadog), текстові логи важко аналізувати.
Production вимагає структурованих логів у форматі JSON.
Для цього використовується бібліотека structlog. Вона перетворює кожен виклик логу на JSON-об'єкт, який легко парситься пошуковими системами.
pip install structlog
uv add structlog
poetry add structlog
Налаштуємо structlog у FastAPI:
import logging
import sys
import structlog
def setup_logging():
# Налаштовуємо стандартне логування для сумісності з іншими бібліотеками
logging.basicConfig(
format="%(message)s",
stream=sys.stdout,
level=logging.INFO,
)
structlog.configure(
processors=[
# Додає інформацію про рівень логування (info, error)
structlog.processors.add_log_level,
# Додає мітку часу
structlog.processors.TimeStamper(fmt="iso"),
# Форматує вивід як JSON рядок
structlog.processors.JSONRenderer()
],
# Направляємо лог у стандартний вивід
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
Якщо викликати такий логгер: logger.info("user_login", user_id=123, ip="127.0.0.1"), у консолі з'явиться структурована інформація:
{
"event": "user_login",
"user_id": 123,
"ip": "127.0.0.1",
"level": "info",
"timestamp": "2026-07-16T10:45:00.123456Z"
}
Ендпоінти перевірки здоров'я: Liveness та Readiness
Для оркестраторів на кшталт Kubernetes критично знати стан контейнера. Існує два типи перевірок:
- Liveness Probe (чи живий додаток?): Перевіряє, чи не завис Event Loop. Якщо ендпоінт не відповідає, Kubernetes перезапустить контейнер.
- Readiness Probe (чи готовий приймати трафік?): Перевіряє, чи є зв'язок з базою даних та Redis. Якщо зв'язку немає, Kubernetes тимчасово відключить контейнер від балансувальника трафіку, щоб користувачі не отримували помилки 500.
Реалізуємо ці перевірки у FastAPI:
from fastapi import FastAPI, status, Response
from app.database import database_check_connection # імітація перевірки з БД
from app.redis import redis_check_connection # імітація перевірки з Redis
from app.logger import logger
app = FastAPI()
# Liveness: Проста швидка відповідь
@app.get("/healthz", status_code=status.HTTP_200_OK)
async def liveness():
return {"status": "alive"}
# Readiness: Глибока перевірка залежностей
@app.get("/readyz")
async def readiness(response: Response):
db_healthy = await database_check_connection()
redis_healthy = await redis_check_connection()
if not db_healthy or not redis_healthy:
logger.error("readiness_check_failed", database=db_healthy, redis=redis_healthy)
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return {"status": "unready", "database": db_healthy, "redis": redis_healthy}
return {"status": "ready"}
Порівняльний аналіз логування: .NET ↔ Python
| Параметр порівняння | .NET Serilog / OpenTelemetry | Python structlog / OpenTelemetry |
|---|---|---|
| Логування в JSON | Зручно конфігурується через WriteTo.Console(new RenderedCompactJsonFormatter()) | Налаштовується через процесори structlog.processors.JSONRenderer |
| Властивості (Enrichers) | Додавання контексту через LogContext.PushProperty("UserId", 123) | Додавання контексту через logger.bind(user_id=123) |
| Експорт метрик | Вбудований інструментарій System.Diagnostics.Metrics | Потребує бібліотек prometheus-client або opentelemetry-sdk |
7. Практичний проєкт від А до Я: Контейнеризація та Деплой "StatusMonitor"
Щоб закріпити здобуті знання на практиці, ми побудуємо з нуля повністю працездатний, задокерований мікросервіс моніторингу статусів StatusMonitor. Цей застосунок зчитуватиме налаштування з оточення, логуватиме події в JSON, перевірятиме з'єднання з базою даних та Redis через /readyz та матиме налаштований пайплайн для автоматичного тестування.
Створіть нову папку проекту status-monitor/ та розмістіть у ній файли згідно з такою структурою:
status-monitor/
├── .github/
│ └── workflows/
│ └── ci.yml
├── app/
│ ├── __init__.py
│ ├── config.py
│ ├── database.py
│ ├── logger.py
│ └── main.py
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
Крок 1: Залежності проекту
Створимо файл requirements.txt з усіма необхідними бібліотеками для нашої продакшн-конфігурації:
fastapi>=0.110.0
uvicorn[standard]>=0.28.0
gunicorn>=22.0.0
pydantic-settings>=2.2.0
python-dotenv>=1.0.1
structlog>=24.1.0
sqlalchemy[asyncio]>=2.0.28
asyncpg>=0.29.0
redis>=5.0.3
prometheus-fastapi-instrumentator>=7.0.0
pytest>=8.1.1
pytest-asyncio>=0.23.5
httpx>=0.27.0
Крок 2: Конфігурація оточення (app/config.py)
Опишемо клас налаштувань, який зчитуватиме змінні системи та валідуватиме їх за допомогою Pydantic:
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import PostgresDsn, RedisDsn
class Settings(BaseSettings):
ENVIRONMENT: str = "production"
SECRET_KEY: str
DATABASE_URL: PostgresDsn
REDIS_URL: RedisDsn
PORT: int = 8000
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8"
)
settings = Settings()
Крок 3: Логування у форматі JSON (app/logger.py)
Налаштуємо логер structlog для виводу повідомлень у структурованому форматі, придатному для систем збору логів:
import logging
import sys
import structlog
def setup_logging():
logging.basicConfig(
format="%(message)s",
stream=sys.stdout,
level=logging.INFO,
)
structlog.configure(
processors=[
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
Крок 4: Підключення до БД та перевірка здоров'я (app/database.py)
Створимо асинхронний рушій SQLAlchemy та функцію для перевірки реального зв'язку з базою даних:
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import text
from app.config import settings
from app.logger import logger
engine = create_async_engine(str(settings.DATABASE_URL), echo=False)
async def check_db_connection() -> bool:
try:
async with engine.connect() as conn:
await conn.execute(text("SELECT 1"))
return True
except Exception as e:
logger.error("database_ping_failed", error=str(e))
return False
Крок 5: Вебдодаток та перевірки здоров'я (app/main.py)
Зберемо наш додаток FastAPI, підключимо структуроване логування, експорт метрик для Prometheus та опишемо ендпоінти перевірок /healthz та /readyz:
import redis.asyncio as aioredis
from fastapi import FastAPI, Response, status
from prometheus_fastapi_instrumentator import Instrumentator
from app.config import settings
from app.logger import setup_logging, logger
from app.database import check_db_connection
# Ініціалізуємо структуровані логи
setup_logging()
app = FastAPI(title="StatusMonitor API")
# Підключаємо Prometheus метрики
Instrumentator().instrument(app).expose(app)
@app.on_event("startup")
async def startup_event():
logger.info("application_starting", environment=settings.ENVIRONMENT)
@app.get("/healthz", status_code=status.HTTP_200_OK)
async def liveness():
# Швидка перевірка, чи не завис Event Loop
return {"status": "healthy"}
@app.get("/readyz")
async def readiness(response: Response):
# Глибока перевірка зв'язку з БД
db_ok = await check_db_connection()
# Глибока перевірка зв'язку з Redis
redis_ok = False
try:
r = aioredis.from_url(str(settings.REDIS_URL))
redis_ok = await r.ping()
except Exception as e:
logger.error("redis_ping_failed", error=str(e))
if not db_ok or not redis_ok:
logger.warn("readiness_check_failed", database=db_ok, redis=redis_ok)
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return {"status": "unready", "database": db_ok, "redis": redis_ok}
return {"status": "ready", "database": db_ok, "redis": redis_ok}
@app.get("/")
async def root():
return {"message": "Welcome to StatusMonitor API"}
Крок 6: Контейнеризація (Dockerfile та .dockerignore)
Створимо оптимізований multi-stage Dockerfile (ми використаємо варіант з pip для демонстрації, оскільки залежності описані в requirements.txt):
# STAGE 1: Builder
FROM python:3.12-slim AS builder
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# STAGE 2: Runner
FROM python:3.12-slim AS runner
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /install /usr/local
# Створюємо безпечного non-root користувача
RUN groupadd -g 10001 appuser && \
useradd -u 10000 -g appuser -m -s /sbin/nologin appuser
COPY ./app ./app
COPY ./main.py .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "main:app", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
Створимо файл .dockerignore, щоб не копіювати непотрібне сміття в образ:
.git
.github
.venv
__pycache__
*.db
.env
Dockerfile
docker-compose.yml
Крок 7: Локальна оркестрація (docker-compose.yml)
Створимо файл для запуску нашого API разом з PostgreSQL та Redis, налаштувавши взаємозв'язок за допомогою Health Checks:
version: '3.8'
services:
db:
image: postgres:16-alpine
container_name: monitor_db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: monitorpassword
POSTGRES_DB: monitordb
ports:
- '5432:5432'
volumes:
- monitor_db_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres -d monitordb']
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: monitor_redis
ports:
- '6379:6379'
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 5
web:
build: .
container_name: monitor_api
ports:
- '8000:8000'
environment:
- ENVIRONMENT=production
- SECRET_KEY=super-secret-status-monitor-key-32-chars
- DATABASE_URL=postgresql+asyncpg://postgres:monitorpassword@db:5432/monitordb
- REDIS_URL=redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
monitor_db_data:
Крок 8: Автоматизація перевірок (.github/workflows/ci.yml)
Створимо GitHub Actions файл для автоматичного лінтингу та тестування:
name: Code Quality CI
on:
push:
branches: ['main']
pull_request:
branches: ['main']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install ruff mypy
- name: Ruff Lint
run: ruff check app/
- name: Mypy Type Check
run: mypy app/
Як запустити та протестувати проєкт:
- Запустіть контейнери:
docker compose up --build -d. - Перевірте статус контейнерів:
docker compose ps. Ви повинні побачити, що всі сервіси перебувають у статусіhealthy. - Зробіть тестовий запит до Liveness Probe:
curl http://localhost:8000/healthz # Відповідь: {"status":"healthy"} - Зробіть запит до Readiness Probe, що перевіряє реальні з'єднання:
curl http://localhost:8000/readyz # Відповідь: {"status":"ready","database":true,"redis":true} - Перегляньте Prometheus метрики, які генеруються автоматично:
curl http://localhost:8000/metrics.
8. Практичні завдання (Practice)
Рівень 1: Контейнеризація додатка за допомогою Docker Compose
- Створіть простенький FastAPI додаток.
- Напишіть для нього
Dockerfileна базіpython:3.12-slim. - Напишіть
docker-compose.yml, який запускає цей FastAPI додаток разом із базою даних PostgreSQL. - Налаштуйте
healthcheckдля БД так, щоб FastAPI запускався лише після того, як база даних буде повністю готова до прийому з'єднань.
Рівень 2: Налаштування CI/CD конвеєра в GitHub Actions
- Створіть новий публічний репозиторій на GitHub та завантажте туди код вашого FastAPI додатка.
- Створіть файл
.github/workflows/ci.yml. - Налаштуйте автоматичний запуск Ruff для лінтингу та перевірки форматування коду при кожному пуші в гілку
mainабо створенні Pull Request. - Додайте крок запуску тестів через Pytest.
Рівень 3: Додавання Prometheus метрик до API
- Встановіть бібліотеку
prometheus-fastapi-instrumentator. - Інтегруйте її у ваш FastAPI додаток.
- Перевірте, що за адресою
/metricsвідображаються системні метрики додатка (кількість запитів, час відповіді). - Запустіть поруч у Docker Compose контейнер Prometheus, налаштуйте збір метрик з вашого API та візуалізуйте їх у Grafana.
Домашнє завдання (TaskForge Practice)
Для успішного завершення цієї теми виконайте наступні зміни у вашому проекті TaskForge:
- Створення multi-stage Dockerfile:
- Напишіть оптимізований
Dockerfileз двома етапами (builderтаrunner). - Налаштуйте запуск додатка від імені не-root користувача (
appuser).
- Напишіть оптимізований
- Створення docker-compose.yml:
- Налаштуйте запуск FastAPI додатка, бази даних PostgreSQL та Redis.
- Забезпечте збереження даних PostgreSQL на диск хост-машини через volumes.
- Пропишіть коректні
healthcheckперевірки.
- Налаштування Pydantic Settings:
- Реалізуйте зчитування конфігурації підключень до БД та Redis через змінні оточення.
- Додавання health check:
- Створіть ендпоінти
/healthz(liveness) та/readyz(readiness) з логуванням стану.
- Створіть ендпоінти
- CI/CD конвеєр:
- Налаштуйте GitHub Actions workflow для автоматичної перевірки коду (
ruff+mypy+pytest).
- Налаштуйте GitHub Actions workflow для автоматичної перевірки коду (
Git Commit:
feat: add Docker, docker-compose, CI/CD and health checks
🎉 Вітаємо! Ви пройшли повний шлях розробки та розгортання API. Тепер ваш проект TaskForge є повністю готовим, стійким та масштабованим production-ready застосунком на базі FastAPI!