feat(infra): стенд целиком — Kafka, Airflow 3, Superset и мониторинг одной командой

Зачем: рядом с кластером ClickHouse не хватало остальной платформы, а
поднимать её по кускам — значит каждый раз вспоминать порядок. Теперь
`make up` даёт стенд целиком, а `make smoke` честно отвечает, работает он
или нет.

Что:
- Kafka в режиме KRaft (без ZooKeeper), Postgres под метаданные, Airflow
  3.3 четырьмя сервисами и Superset 6.1 с драйверами ClickHouse;
- мониторинг: Prometheus снимает метрики с обеих нод ClickHouse и keeper,
  Grafana получает подготовленный источник данных;
- подключение Airflow ведёт на ноду 1, подключение Superset — на ноду 2:
  ловушка правильных ошибок, забытый `ON CLUSTER` виден в дашборде сам;
- `scripts/stand-smoke.sh` — сквозная проверка из 24 пунктов: топик в
  Kafka, цели Prometheus, запуск примера DAG через API Airflow, проверка
  подключения Superset и расход памяти против порога 3,4 ГБ;
- `make config-test` — статические ворота: Compose, синтаксис Bash и
  Python, стражи README; стражи smoke проверяют, что отчёт краснеет на
  сломанном стенде и зеленеет после восстановления;
- решения записаны в `docs/adr/0001-stand-services.md`, состав стенда и
  порядок работы — в README.

Проверка: на чистых томах `make clean` → `cp .env.example .env` →
`make up` (1 мин 51 с) → `make smoke` — 24 пройдено, 0 ошибок, 2171 MiB.
Перезапуск `make down` → `make up` → `make smoke` — 24/0. Также зелены
`make config-test` (3/0 и 5/0), `make smoke-cluster` (8/8) и
`make smoke-guards` (3/0 и 3/0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 08:46:04 +03:00
co-authored by Claude Opus 5
parent dca0142fd1
commit 63c42bc068
24 changed files with 1493 additions and 31 deletions
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
install -d -o airflow -g root -m 0775 /opt/airflow/auth
# SimpleAuthManager не умеет создавать пользователей через CLI: пароль хранится в JSON.
runuser -u airflow -- python - <<'PY'
import json
import os
from pathlib import Path
passwords_file = Path(os.environ["AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_PASSWORDS_FILE"])
passwords_file.write_text(
json.dumps(
{os.environ["AIRFLOW_ADMIN_USER"]: os.environ["AIRFLOW_ADMIN_PASSWORD"]},
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)
PY
runuser -u airflow -- airflow db migrate
runuser -u airflow -- airflow connections delete clickhouse_default >/dev/null 2>&1 || true
runuser -u airflow -- airflow connections add clickhouse_default \
--conn-type generic \
--conn-host clickhouse-01 \
--conn-port 8123 \
--conn-login default \
--conn-schema default \
--conn-description "ClickHouse, нода 1; типизированный провайдер появится на этапе ETL"
+11
View File
@@ -0,0 +1,11 @@
<clickhouse>
<!-- Встроенная точка метрик проверена на образе ClickHouse 26.3.17.56. -->
<prometheus>
<endpoint>/metrics</endpoint>
<port>9363</port>
<metrics>true</metrics>
<events>true</events>
<asynchronous_metrics>true</asynchronous_metrics>
<errors>true</errors>
</prometheus>
</clickhouse>
+9 -1
View File
@@ -6,6 +6,15 @@
</logger>
<listen_host>0.0.0.0</listen_host>
<!-- Метрики keeper; схема проверена на образе ClickHouse 26.3.17.56. -->
<prometheus>
<endpoint>/metrics</endpoint>
<port>9363</port>
<metrics>true</metrics>
<events>true</events>
<asynchronous_metrics>true</asynchronous_metrics>
</prometheus>
<keeper_server>
<tcp_port>9181</tcp_port>
<server_id>1</server_id>
@@ -24,4 +33,3 @@
</raft_configuration>
</keeper_server>
</clickhouse>
@@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
# Один сервер, но отдельные владельцы и базы не смешивают метаданные приложений.
psql --set=ON_ERROR_STOP=1 \
--set=airflow_user="$AIRFLOW_METADATA_USER" \
--set=airflow_password="$AIRFLOW_METADATA_PASSWORD" \
--set=superset_user="$SUPERSET_METADATA_USER" \
--set=superset_password="$SUPERSET_METADATA_PASSWORD" \
--username "$POSTGRES_USER" \
--dbname postgres <<'SQL'
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'airflow_user', :'airflow_password')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'airflow_user') \gexec
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'superset_user', :'superset_password')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'superset_user') \gexec
SELECT format('CREATE DATABASE airflow OWNER %I', :'airflow_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'airflow') \gexec
SELECT format('CREATE DATABASE superset OWNER %I', :'superset_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'superset') \gexec
SQL
+18
View File
@@ -0,0 +1,18 @@
global:
scrape_interval: 15s
scrape_timeout: 10s
scrape_configs:
- job_name: clickhouse
static_configs:
- targets:
- clickhouse-01:9363
- clickhouse-02:9363
labels:
role: server
- job_name: clickhouse-keeper
static_configs:
- targets:
- clickhouse-keeper:9363
labels:
role: keeper
+12
View File
@@ -0,0 +1,12 @@
ARG SUPERSET_BASE_IMAGE
FROM ${SUPERSET_BASE_IMAGE}
USER root
# В основном образе нет ни драйвера ClickHouse, ни драйвера Postgres.
RUN . /app/.venv/bin/activate \
&& uv pip install --no-cache \
"clickhouse-connect==1.6.0" \
"psycopg2-binary==2.9.12"
USER superset
@@ -0,0 +1,14 @@
database_name: ClickHouse
sqlalchemy_uri: clickhousedb://default@clickhouse-02:8123/default
uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d
cache_timeout: null
expose_in_sqllab: true
allow_run_async: false
allow_ctas: false
allow_cvas: false
allow_dml: false
allow_file_upload: false
impersonate_user: false
extra:
allows_virtual_table_explore: true
version: 1.0.0
+3
View File
@@ -0,0 +1,3 @@
version: 1.0.0
type: Database
timestamp: '2026-07-30T00:00:00+00:00'
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
superset db upgrade
if python - <<'PY'
import os
from sqlalchemy import create_engine, text
engine = create_engine(os.environ["SUPERSET__SQLALCHEMY_DATABASE_URI"])
with engine.connect() as connection:
exists = connection.scalar(
text("SELECT EXISTS (SELECT 1 FROM ab_user WHERE username = :username)"),
{"username": os.environ["SUPERSET_ADMIN_USER"]},
)
raise SystemExit(0 if exists else 1)
PY
then
superset fab reset-password \
--username "$SUPERSET_ADMIN_USER" \
--password "$SUPERSET_ADMIN_PASSWORD"
else
superset fab create-admin \
--username "$SUPERSET_ADMIN_USER" \
--password "$SUPERSET_ADMIN_PASSWORD" \
--firstname Superset \
--lastname Admin \
--email admin@example.invalid
fi
superset init
superset import-directory /app/import --overwrite
+17
View File
@@ -0,0 +1,17 @@
"""Минимальные настройки локального учебного Superset."""
import os
SQLALCHEMY_DATABASE_URI = os.environ["SUPERSET__SQLALCHEMY_DATABASE_URI"]
SECRET_KEY = os.environ["SUPERSET_SECRET_KEY"]
SESSION_COOKIE_NAME = "superset_session"
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
WTF_CSRF_ENABLED = True
CACHE_CONFIG = {
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 300,
}