170 lines
9.0 KiB
Markdown
170 lines
9.0 KiB
Markdown
# ТЗ: Healthcheck enduro-trails-app — заменить curl на python-проверку
|
||
|
||
**Work Item:** ET-015
|
||
**Базовый документ:** [01-brd.md](01-brd.md)
|
||
**Версия:** 1.0
|
||
**Дата:** 2026-06-05
|
||
|
||
## 1. Постановка
|
||
|
||
Заменить в `docker-compose.yml` (сервис `app`) healthcheck-команду
|
||
так, чтобы она:
|
||
- использовала средства, **уже доступные** в образе `python:3.12-slim`
|
||
(т.е. интерпретатор `python3`), без установки дополнительных пакетов;
|
||
- обращалась к `http://localhost:5556/api/health` и трактовала
|
||
HTTP-код **2xx** как «healthy», любой иной отклик и любую ошибку
|
||
соединения — как «unhealthy»;
|
||
- укладывалась в `timeout: 5s`.
|
||
|
||
## 2. Текущее состояние
|
||
|
||
`docker-compose.yml`, строки 22–26:
|
||
|
||
```yaml
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-f", "http://localhost:5556/api/health"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 3
|
||
```
|
||
|
||
`Dockerfile`: базовый образ `python:3.12-slim`. `curl` отсутствует.
|
||
Установлен `pip`, доступен `python3` (и алиас `python`).
|
||
|
||
`src/api/main.py:1224`:
|
||
|
||
```python
|
||
@app.get("/api/health")
|
||
async def health():
|
||
return {
|
||
"status": "ok",
|
||
"db_path": DATA_PATH,
|
||
"db_exists": os.path.exists(DATA_PATH),
|
||
}
|
||
```
|
||
|
||
Возвращает HTTP 200 + JSON. Менять не требуется.
|
||
|
||
## 3. Целевое состояние
|
||
|
||
### 3.1. Изменение в `docker-compose.yml`
|
||
|
||
Секция `healthcheck` сервиса `app` приводится к виду:
|
||
|
||
```yaml
|
||
healthcheck:
|
||
test:
|
||
- "CMD"
|
||
- "python"
|
||
- "-c"
|
||
- "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:5556/api/health', timeout=3).status == 200 else 1)"
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 3
|
||
start_period: 20s
|
||
```
|
||
|
||
Пояснения:
|
||
- `CMD` (а не `CMD-SHELL`) — никакого shell-парсинга, аргументы передаются
|
||
как массив, экранирование не нужно.
|
||
- `python` — алиас, имеющийся в `python:3.12-slim` (есть и `python3`,
|
||
оба указывают на один интерпретатор).
|
||
- `urllib.request.urlopen(..., timeout=3)` — стандартная библиотека,
|
||
без зависимостей; внутренний `timeout=3` короче внешнего
|
||
`timeout: 5s`, остаётся запас на старт интерпретатора.
|
||
- `sys.exit(0 if ... == 200 else 1)` — корректное преобразование
|
||
статуса HTTP в exit code. Любой raise (URLError, HTTPError, timeout)
|
||
пробросится наверх, процесс завершится ненулевым кодом → `unhealthy`.
|
||
- `start_period: 20s` — добавляется, чтобы Docker не считал ранние
|
||
ошибки запуска приложения «провалом» healthcheck в окне старта.
|
||
Uvicorn поднимается за < 2 c, 20 с — комфортный запас.
|
||
|
||
### 3.2. Изменения в `Dockerfile`
|
||
|
||
**Не требуются.** Добавлять `curl` через `apt-get` нельзя — раздувает
|
||
образ и противоречит выбранному подходу.
|
||
|
||
### 3.3. Изменения в `src/api/main.py`
|
||
|
||
**Не требуются.** Эндпоинт `/api/health` существует и отдаёт 200.
|
||
|
||
## 4. Альтернативы (рассмотрены и отклонены)
|
||
|
||
| Вариант | Плюсы | Минусы | Решение |
|
||
|---------|-------|--------|---------|
|
||
| `apt-get install curl` в Dockerfile | Привычная команда | +~10 МБ к образу, новый APT-слой, противоречит slim-философии | Отклонено |
|
||
| `wget --spider` | Однострочник | `wget` тоже отсутствует в `python:3.12-slim` (проверено: пакет `wget` не входит в slim) | Отклонено |
|
||
| HEALTHCHECK в Dockerfile | Декларативно | Дублирует compose, при изменении нужно пересобирать образ | Отклонено, держим в compose |
|
||
| Отдельный health-скрипт `scripts/healthcheck.py` | Чище YAML | Лишний файл для одной строки, мутит образ | Отклонено |
|
||
|
||
Принятый вариант: **inline python one-liner** через `urllib.request`.
|
||
|
||
## 5. Реализационные требования
|
||
|
||
### R-1. Изменение `docker-compose.yml`
|
||
- В сервисе `app` секция `healthcheck` заменяется на конструкцию из
|
||
п. 3.1.
|
||
- Остальные параметры сервиса (ports, volumes, environment) не
|
||
затрагиваются.
|
||
|
||
### R-2. Идемпотентность пересборки
|
||
- Изменения не требуют ребилда образа (`docker compose build`).
|
||
Достаточно `docker compose up -d app` для пересоздания контейнера
|
||
с новой healthcheck-командой.
|
||
- Допускается ребилд при необходимости — это не должно ломать сборку.
|
||
|
||
### R-3. Обратная совместимость
|
||
- Никаких ENV-переменных, влияющих на путь healthcheck, не вводится.
|
||
Адрес `http://localhost:5556/api/health` зашит в строку.
|
||
(Локальный — `localhost` внутри контейнера; порт всегда 5556,
|
||
как ENV `PORT` в Dockerfile.)
|
||
|
||
### R-4. Документация
|
||
- В `docs/work-items/ET-015/06-adr/healthcheck-via-python.md` зафиксировать
|
||
решение «использовать python-one-liner вместо curl». Автор ADR —
|
||
следующий этап (Architecture), не Анализ.
|
||
- Обновить `CHANGELOG.md` в секции «Unreleased» строкой формата
|
||
`fix(infra): use python urllib for container healthcheck (ET-015)`.
|
||
|
||
### R-5. Линт и форматирование
|
||
- YAML-валидность `docker-compose.yml` проверяется `make lint`.
|
||
- Длина строки python one-liner допустима в YAML (нет лимита 120 для
|
||
строковых значений multi-line array).
|
||
|
||
## 6. Тестирование
|
||
|
||
См. [04-test-plan.yaml](04-test-plan.yaml). Кратко:
|
||
|
||
- **integration-1**: после `docker compose up -d app` контейнер должен
|
||
выйти в `healthy` за ≤ 120 с.
|
||
- **integration-2**: при остановке uvicorn (или искусственном блоке
|
||
порта) — за ≤ 120 с переходит в `unhealthy`.
|
||
- **unit-1** (опционально): smoke-тест python-one-liner вне Docker
|
||
через `python -c "..."` против поднятого локально `make dev`.
|
||
|
||
## 7. Деплой и откат
|
||
|
||
- Деплой: `make deploy-test` (как обычно). При деплое compose
|
||
пересоздаст контейнер `enduro-trails-app-1`.
|
||
- Проверка: `docker inspect enduro-trails-app-1 --format
|
||
'{{.State.Health.Status}}'` → `healthy` в течение нескольких циклов
|
||
(`interval=30s × 3 = 90s` плюс `start_period=20s`).
|
||
- Откат: `git revert` коммита; `docker compose up -d app`. Старая
|
||
(поломанная) healthcheck-команда вернётся, но сам сервис продолжит
|
||
работать.
|
||
|
||
## 8. Риски
|
||
|
||
| Риск | Вероятность | Митигация |
|
||
|------|-------------|-----------|
|
||
| Python one-liner крэшится на каком-то Docker-движке из-за квотинга | низкая | YAML-массив `["CMD", "python", "-c", "..."]` — без shell, без экранирования |
|
||
| Длинная строка усложняет редактирование | средняя | Использовать YAML block-scalar (`>-` или `|`) при необходимости, но в текущей форме строка читаемая |
|
||
| Эндпоинт `/api/health` в будущем сделают «дорогим» и timeout=3s не хватит | низкая | Эндпоинт сейчас отдаёт ~7 мс; при изменении — пересмотр timeout |
|
||
| На prod-среде iptables/сеть отличаются и localhost внутри контейнера ведёт себя иначе | очень низкая | `localhost` в network namespace контейнера = loopback контейнера, не зависит от хоста |
|
||
|
||
## 9. Definition of Ready (для следующего этапа)
|
||
|
||
- BRD прочитан, ТЗ согласовано.
|
||
- Доступ к тестовой среде mva154 для проверки `docker inspect`.
|
||
- `make deploy-test` и `docker compose` доступны из ветки.
|