Merge pull request 'feat(clickhouse): реализована модель доступа' (#82) from feat/78-clickhouse-access into main
Reviewed-on: #82
This commit was merged in pull request #82.
This commit is contained in:
@@ -4,6 +4,11 @@ CLICKHOUSE_01_HTTP_PORT=28123
|
|||||||
CLICKHOUSE_01_TCP_PORT=29000
|
CLICKHOUSE_01_TCP_PORT=29000
|
||||||
CLICKHOUSE_02_HTTP_PORT=28124
|
CLICKHOUSE_02_HTTP_PORT=28124
|
||||||
CLICKHOUSE_02_TCP_PORT=29001
|
CLICKHOUSE_02_TCP_PORT=29001
|
||||||
|
CLICKHOUSE_CLUSTER_SECRET=cluster-26-secret
|
||||||
|
CLICKHOUSE_DEFAULT_PASSWORD=default-26-local
|
||||||
|
CLICKHOUSE_ETL_PASSWORD=etl-26-writer
|
||||||
|
CLICKHOUSE_BI_PASSWORD=bi-26-reader
|
||||||
|
CLICKHOUSE_ANALYST_PASSWORD=analyst-26-reader
|
||||||
KAFKA_EXTERNAL_PORT=29092
|
KAFKA_EXTERNAL_PORT=29092
|
||||||
PROMETHEUS_PORT=29090
|
PROMETHEUS_PORT=29090
|
||||||
GRAFANA_PORT=23000
|
GRAFANA_PORT=23000
|
||||||
|
|||||||
@@ -242,16 +242,27 @@ uv run --project generator python -m clickstream_generator batch \
|
|||||||
- Prometheus — `http://127.0.0.1:29090`;
|
- Prometheus — `http://127.0.0.1:29090`;
|
||||||
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
|
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
|
||||||
|
|
||||||
У локального учебного кластера нет пароля: ноды используют общего пользователя
|
В ClickHouse четыре пользователя:
|
||||||
`default` для запросов `Distributed`. Порты поэтому привязаны к `127.0.0.1` и
|
|
||||||
не открыты во внешнюю сеть.
|
|
||||||
|
|
||||||
Пароли интерфейсов, пароли Postgres, ключи Airflow и Superset, отсутствие
|
- `etl` применяет DDL и подключает Airflow; роль `etl_writer` читает и пишет
|
||||||
пароля ClickHouse и отсутствие проверки доступа у Kafka и Prometheus —
|
слои хранилища;
|
||||||
намеренно простые и явно ненастоящие настройки локального учебного стенда. Это
|
- `bi` подключает Superset; роль `bi_reader` читает будущие слои DDS и DM;
|
||||||
не пример настройки защиты: не копируйте значения из `.env.example` в рабочую
|
- `analyst` предназначен для подключения человека и читает все слои;
|
||||||
среду. Все опубликованные порты привязаны только к `127.0.0.1`; Postgres наружу
|
- `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри
|
||||||
не опубликован.
|
контейнеров, но не приложения.
|
||||||
|
|
||||||
|
Решение и его доводы — в [ADR 0007](docs/adr/0007-clickhouse-access.md).
|
||||||
|
Пользователи и роли объявлены в `infra/clickhouse/users.d/access.xml`. Пароли и
|
||||||
|
общий секрет нод живут в `.env` и передаются в конфигурацию через окружение;
|
||||||
|
Superset получает пароль `bi` тем же путём через штатную функцию настройки.
|
||||||
|
Значения для локального стенда есть в `.env.example`.
|
||||||
|
|
||||||
|
Пароли ClickHouse и интерфейсов, пароли Postgres, ключи Airflow и Superset,
|
||||||
|
отсутствие проверки доступа у Kafka и Prometheus — намеренно простые и явно
|
||||||
|
ненастоящие настройки локального учебного стенда. Это не пример настройки
|
||||||
|
защиты: не копируйте значения из `.env.example` в рабочую среду. Все
|
||||||
|
опубликованные порты привязаны только к `127.0.0.1`; Postgres наружу не
|
||||||
|
опубликован.
|
||||||
|
|
||||||
В бою перед репликами ClickHouse обычно был бы балансировщик. Здесь в каждом
|
В бою перед репликами ClickHouse обычно был бы балансировщик. Здесь в каждом
|
||||||
шарде одна реплика, поэтому балансировать нечего. Балансировщик и топология
|
шарде одна реплика, поэтому балансировать нечего. Балансировщик и топология
|
||||||
|
|||||||
+16
-4
@@ -9,8 +9,13 @@ x-clickhouse-common: &clickhouse-common
|
|||||||
image: clickhouse/clickhouse-server:26.3.17.56
|
image: clickhouse/clickhouse-server:26.3.17.56
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# Стенд локальный: нодам нужен общий пользователь без пароля для Distributed.
|
# Пользователей объявляет access.xml; настройка учётки образом не нужна.
|
||||||
CLICKHOUSE_SKIP_USER_SETUP: "1"
|
CLICKHOUSE_SKIP_USER_SETUP: "1"
|
||||||
|
CLICKHOUSE_CLUSTER_SECRET: ${CLICKHOUSE_CLUSTER_SECRET:?Скопируйте .env.example в .env}
|
||||||
|
CLICKHOUSE_DEFAULT_PASSWORD: ${CLICKHOUSE_DEFAULT_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
|
CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
|
CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
|
CLICKHOUSE_ANALYST_PASSWORD: ${CLICKHOUSE_ANALYST_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
depends_on:
|
depends_on:
|
||||||
clickhouse-keeper:
|
clickhouse-keeper:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -18,7 +23,7 @@ x-clickhouse-common: &clickhouse-common
|
|||||||
# Гигабайта не хватало: половину съедали страницы самого бинарника (ADR 0004).
|
# Гигабайта не хватало: половину съедали страницы самого бинарника (ADR 0004).
|
||||||
mem_limit: 4g
|
mem_limit: 4g
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --query 'SELECT 1' >/dev/null 2>&1"]
|
test: ["CMD-SHELL", "clickhouse-client --host 127.0.0.1 --user default --password \"$$CLICKHOUSE_DEFAULT_PASSWORD\" --query 'SELECT 1' >/dev/null 2>&1"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 30
|
retries: 30
|
||||||
@@ -44,6 +49,7 @@ x-airflow-common: &airflow-common
|
|||||||
AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: "true"
|
AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK: "true"
|
||||||
AIRFLOW_ADMIN_USER: ${AIRFLOW_ADMIN_USER:?Скопируйте .env.example в .env}
|
AIRFLOW_ADMIN_USER: ${AIRFLOW_ADMIN_USER:?Скопируйте .env.example в .env}
|
||||||
AIRFLOW_ADMIN_PASSWORD: ${AIRFLOW_ADMIN_PASSWORD:?Скопируйте .env.example в .env}
|
AIRFLOW_ADMIN_PASSWORD: ${AIRFLOW_ADMIN_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
|
CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
volumes:
|
volumes:
|
||||||
- ./dags:/opt/airflow/dags:ro
|
- ./dags:/opt/airflow/dags:ro
|
||||||
- ./infra/airflow/init.sh:/opt/airflow/init.sh:ro
|
- ./infra/airflow/init.sh:/opt/airflow/init.sh:ro
|
||||||
@@ -82,6 +88,7 @@ x-superset-common: &superset-common
|
|||||||
SUPERSET__SQLALCHEMY_DATABASE_URI: "postgresql+psycopg2://${SUPERSET_METADATA_USER:?Скопируйте .env.example в .env}:${SUPERSET_METADATA_PASSWORD:?Скопируйте .env.example в .env}@postgres-metadata:5432/superset"
|
SUPERSET__SQLALCHEMY_DATABASE_URI: "postgresql+psycopg2://${SUPERSET_METADATA_USER:?Скопируйте .env.example в .env}:${SUPERSET_METADATA_PASSWORD:?Скопируйте .env.example в .env}@postgres-metadata:5432/superset"
|
||||||
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:?Скопируйте .env.example в .env}
|
SUPERSET_ADMIN_USER: ${SUPERSET_ADMIN_USER:?Скопируйте .env.example в .env}
|
||||||
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:?Скопируйте .env.example в .env}
|
SUPERSET_ADMIN_PASSWORD: ${SUPERSET_ADMIN_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
|
CLICKHOUSE_BI_PASSWORD: ${CLICKHOUSE_BI_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
volumes:
|
volumes:
|
||||||
- ./infra/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
|
- ./infra/superset/superset_config.py:/app/pythonpath/superset_config.py:ro
|
||||||
- ./infra/superset/init.sh:/app/infra/init.sh:ro
|
- ./infra/superset/init.sh:/app/infra/init.sh:ro
|
||||||
@@ -109,7 +116,7 @@ services:
|
|||||||
retries: 30
|
retries: 30
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
# Инициатор DDL и будущее подключение Airflow.
|
# Инициатор DDL и точка подключения Airflow.
|
||||||
clickhouse-01:
|
clickhouse-01:
|
||||||
<<: *clickhouse-common
|
<<: *clickhouse-common
|
||||||
hostname: clickhouse-01
|
hostname: clickhouse-01
|
||||||
@@ -120,9 +127,10 @@ services:
|
|||||||
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
|
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
|
||||||
- ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
|
- ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
|
||||||
- ./infra/clickhouse/config.d/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml:ro
|
- ./infra/clickhouse/config.d/macros-01.xml:/etc/clickhouse-server/config.d/macros.xml:ro
|
||||||
|
- ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro
|
||||||
- clickhouse_01_data:/var/lib/clickhouse
|
- clickhouse_01_data:/var/lib/clickhouse
|
||||||
|
|
||||||
# Будущее подключение Superset.
|
# Точка подключения Superset.
|
||||||
clickhouse-02:
|
clickhouse-02:
|
||||||
<<: *clickhouse-common
|
<<: *clickhouse-common
|
||||||
hostname: clickhouse-02
|
hostname: clickhouse-02
|
||||||
@@ -133,6 +141,7 @@ services:
|
|||||||
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
|
- ./infra/clickhouse/config.d/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml:ro
|
||||||
- ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
|
- ./infra/clickhouse/config.d/prometheus.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
|
||||||
- ./infra/clickhouse/config.d/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml:ro
|
- ./infra/clickhouse/config.d/macros-02.xml:/etc/clickhouse-server/config.d/macros.xml:ro
|
||||||
|
- ./infra/clickhouse/users.d/access.xml:/etc/clickhouse-server/users.d/access.xml:ro
|
||||||
- clickhouse_02_data:/var/lib/clickhouse
|
- clickhouse_02_data:/var/lib/clickhouse
|
||||||
|
|
||||||
kafka:
|
kafka:
|
||||||
@@ -225,6 +234,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
LC_ALL: C
|
LC_ALL: C
|
||||||
|
CLICKHOUSE_ETL_PASSWORD: ${CLICKHOUSE_ETL_PASSWORD:?Скопируйте .env.example в .env}
|
||||||
entrypoint: ["/bin/bash", "-ec"]
|
entrypoint: ["/bin/bash", "-ec"]
|
||||||
command:
|
command:
|
||||||
- |
|
- |
|
||||||
@@ -237,6 +247,8 @@ services:
|
|||||||
printf 'Применяем %s.\n' "$${ddl_file##*/}"
|
printf 'Применяем %s.\n' "$${ddl_file##*/}"
|
||||||
clickhouse-client \
|
clickhouse-client \
|
||||||
--host clickhouse-01 \
|
--host clickhouse-01 \
|
||||||
|
--user etl \
|
||||||
|
--password "$$CLICKHOUSE_ETL_PASSWORD" \
|
||||||
--multiquery \
|
--multiquery \
|
||||||
--queries-file "$$ddl_file"
|
--queries-file "$$ddl_file"
|
||||||
done
|
done
|
||||||
|
|||||||
+37
-14
@@ -29,18 +29,31 @@ EXPECTED_TABLES = [
|
|||||||
(LOCAL_TABLE, "ReplicatedMergeTree"),
|
(LOCAL_TABLE, "ReplicatedMergeTree"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Ноду 2 пробник читает не своим подключением, а запросом remote() с ноды 1: у
|
# У Airflow одно подключение — к ноде 1; второго ради пробника не заводят.
|
||||||
# Airflow подготовлено одно подключение — к clickhouse-01, и второго ради
|
# Ноду 2 он читает через remote(), который не использует секрет из описания
|
||||||
# пробника не заводят. При этом remote('clickhouse-02:9000', ...) делает
|
# кластера, поэтому учётные данные etl передаются явно. Порт 9000 — нативный,
|
||||||
# инициатором распределённого запроса саму ноду 2 — проверяется именно это, а
|
# тогда как подключение Airflow ходит по HTTP на 8123. Trace-журнал сервера
|
||||||
# не доступность ноды 2 по сети. Порт 9000 — межсерверный, тогда как
|
# видит пароль: это допустимо только для локального учебного стенда.
|
||||||
# подключение Airflow ходит по HTTP на 8123.
|
|
||||||
NODES = (
|
NODES = (
|
||||||
("ноде 1", "system.tables"),
|
("ноде 1", "system.tables"),
|
||||||
("ноде 2", "remote('clickhouse-02:9000', system.tables)"),
|
(
|
||||||
|
"ноде 2",
|
||||||
|
"""remote(
|
||||||
|
'clickhouse-02:9000', 'system', 'tables',
|
||||||
|
{remote_user:String}, {remote_password:String}
|
||||||
|
)""",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _remote_parameters() -> dict[str, str]:
|
||||||
|
connection = Connection.get("clickhouse_default")
|
||||||
|
return {
|
||||||
|
"remote_user": connection.login,
|
||||||
|
"remote_password": connection.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _clickhouse_client():
|
def _clickhouse_client():
|
||||||
# clickhouse_connect импортируется внутри функции, а не наверху файла:
|
# clickhouse_connect импортируется внутри функции, а не наверху файла:
|
||||||
# обработчик DAG разбирает этот файл снова и снова, и импорт наверху
|
# обработчик DAG разбирает этот файл снова и снова, и импорт наверху
|
||||||
@@ -52,8 +65,8 @@ def _clickhouse_client():
|
|||||||
return clickhouse_connect.get_client(
|
return clickhouse_connect.get_client(
|
||||||
host=connection.host,
|
host=connection.host,
|
||||||
port=connection.port,
|
port=connection.port,
|
||||||
username=connection.login or "default",
|
username=connection.login,
|
||||||
password=connection.password or "",
|
password=connection.password,
|
||||||
database=connection.schema or "default",
|
database=connection.schema or "default",
|
||||||
connect_timeout=5,
|
connect_timeout=5,
|
||||||
send_receive_timeout=30,
|
send_receive_timeout=30,
|
||||||
@@ -68,7 +81,8 @@ def _table_engines(client, source: str) -> list[tuple[str, str]]:
|
|||||||
WHERE database = 'default'
|
WHERE database = 'default'
|
||||||
AND name IN ('{LOCAL_TABLE}', '{DISTRIBUTED_TABLE}')
|
AND name IN ('{LOCAL_TABLE}', '{DISTRIBUTED_TABLE}')
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
"""
|
""",
|
||||||
|
parameters=_remote_parameters(),
|
||||||
)
|
)
|
||||||
return result.result_rows
|
return result.result_rows
|
||||||
|
|
||||||
@@ -173,8 +187,12 @@ def test_clickhouse():
|
|||||||
node_2_rows = client.query(
|
node_2_rows = client.query(
|
||||||
"""
|
"""
|
||||||
SELECT hostName()
|
SELECT hostName()
|
||||||
FROM remote('clickhouse-02:9000', system.one)
|
FROM remote(
|
||||||
"""
|
'clickhouse-02:9000', 'system', 'one',
|
||||||
|
{remote_user:String}, {remote_password:String}
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
parameters=_remote_parameters(),
|
||||||
).result_rows
|
).result_rows
|
||||||
if len(node_2_rows) != 1:
|
if len(node_2_rows) != 1:
|
||||||
raise RuntimeError(f"не удалось определить имя ноды 2: {node_2_rows}")
|
raise RuntimeError(f"не удалось определить имя ноды 2: {node_2_rows}")
|
||||||
@@ -184,11 +202,16 @@ def test_clickhouse():
|
|||||||
FROM remote(
|
FROM remote(
|
||||||
'clickhouse-02:9000',
|
'clickhouse-02:9000',
|
||||||
'default',
|
'default',
|
||||||
'{DISTRIBUTED_TABLE}'
|
'{DISTRIBUTED_TABLE}',
|
||||||
|
{{remote_user:String}},
|
||||||
|
{{remote_password:String}}
|
||||||
)
|
)
|
||||||
WHERE marker = {{marker:String}}
|
WHERE marker = {{marker:String}}
|
||||||
""",
|
""",
|
||||||
parameters={"marker": written["marker"]},
|
parameters={
|
||||||
|
"marker": written["marker"],
|
||||||
|
**_remote_parameters(),
|
||||||
|
},
|
||||||
).result_rows
|
).result_rows
|
||||||
if written["hostname"] == node_2_rows[0][0]:
|
if written["hostname"] == node_2_rows[0][0]:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
@@ -63,11 +63,9 @@ DDS под ними, потому что так чаще всего и быва
|
|||||||
данными по репликам, и там пароль вписан в каждую реплику каждого из трёх
|
данными по репликам, и там пароль вписан в каждую реплику каждого из трёх
|
||||||
кластеров.
|
кластеров.
|
||||||
|
|
||||||
Довод опирается на измеренное лишь наполовину: колонку `user` видно на живом
|
Довод подтверждён замером 13 августа 2026 года: на соседнем шарде сохранился
|
||||||
стенде, а вот что права `bi` на соседнем шарде подменяются правами общей
|
пользователь-инициатор, поэтому там применились его права. Результат замера —
|
||||||
учётки — вывод из неё, и он ждёт измерения (см. конец документа). Окажись
|
в разделе «Что проверено».
|
||||||
вывод неверным, выбор секрета устоит по остальным основаниям, но перестанет
|
|
||||||
быть единственно возможным.
|
|
||||||
|
|
||||||
**Файлы, а не SQL.** Объявленное файлом не оставляет состояния в томах:
|
**Файлы, а не SQL.** Объявленное файлом не оставляет состояния в томах:
|
||||||
повторный `make up` на живом стенде ничего не сдвигает, правка пароля
|
повторный `make up` на живом стенде ничего не сдвигает, правка пароля
|
||||||
@@ -132,12 +130,16 @@ Postgres и Grafana. Цена выбора известна: объявленн
|
|||||||
настройки ClickHouse.
|
настройки ClickHouse.
|
||||||
- Весь DDL репозитория идёт через `IF NOT EXISTS` (файлы `sql/ddl/`).
|
- Весь DDL репозитория идёт через `IF NOT EXISTS` (файлы `sql/ddl/`).
|
||||||
|
|
||||||
Осталось проверить при исполнении, и это работа тикета реализации: точная
|
Проверено на живом стенде 13 августа 2026 года, ClickHouse 26.3.17.56.
|
||||||
форма объявления секрета в описании кластера (готового примера в
|
|
||||||
документации не нашлось); работает ли подстановка из окружения именно для
|
- Секрет кластера принимается внутри `remote_servers` в виде
|
||||||
пароля пользователя, и если нет — пароли лягут в файл настройки прямым
|
`<secret from_env="CLICKHOUSE_CLUSTER_SECRET"/>`. Запрос от `etl` через обе
|
||||||
текстом, а README назовёт это вслух; применяются ли на соседнем шарде права
|
ноды выполнился на каждой под пользователем `etl`.
|
||||||
`bi`, а не общей учётки (вывод из колонки `user`, живьём не измерен —
|
- `from_env` работает у элемента `password`: `etl`, `bi` и `analyst` вошли с
|
||||||
проверка требует создать пользователя); принимает ли ClickHouse права,
|
паролями из окружения и получили свои роли.
|
||||||
выданные на ещё не созданную базу — на момент решения `dds` и `dm` не
|
- При запросе от `bi` через Distributed на второй ноде `system.query_log`
|
||||||
существуют.
|
показал `user = bi` и `initial_user = bi`. Значит, на соседнем шарде
|
||||||
|
применяются права инициатора, а не общей учётной записи.
|
||||||
|
- Права на отсутствующие базы `dds` и `dm` приняты. До создания баз они видны
|
||||||
|
в `SHOW GRANTS FOR etl_writer`; временная таблица в `dm` подтвердила право
|
||||||
|
`bi` на чтение после появления базы.
|
||||||
|
|||||||
@@ -576,6 +576,8 @@ v2 стартует пустым, поэтому объём ниже — это
|
|||||||
стенд работает, но учебного пути на нём ещё нет.
|
стенд работает, но учебного пути на нём ещё нет.
|
||||||
- Инкрементальный ETL (#8) — свой issue.
|
- Инкрементальный ETL (#8) — свой issue.
|
||||||
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
||||||
|
- TLS между нодами, настоящее хранилище и ротация секретов, внешние учётные
|
||||||
|
записи, квоты, политики строк и аудит запросов — за границей учебного стенда.
|
||||||
- Полный словарь торговых событий Метрики (detail, remove, impressions),
|
- Полный словарь торговых событий Метрики (detail, remove, impressions),
|
||||||
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
|
пять уровней категорий, блоки `purchasedProduct*`/`impressions*`.
|
||||||
- Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток
|
- Механика `Sign`/CollapsingMergeTree — кандидат на потом (дом — поток
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ runuser -u airflow -- airflow connections add clickhouse_default \
|
|||||||
--conn-type generic \
|
--conn-type generic \
|
||||||
--conn-host clickhouse-01 \
|
--conn-host clickhouse-01 \
|
||||||
--conn-port 8123 \
|
--conn-port 8123 \
|
||||||
--conn-login default \
|
--conn-login etl \
|
||||||
|
--conn-password "$CLICKHOUSE_ETL_PASSWORD" \
|
||||||
--conn-schema default \
|
--conn-schema default \
|
||||||
--conn-description "ClickHouse, нода 1; типизированный провайдер появится на этапе ETL"
|
--conn-description "ClickHouse, нода 1; типизированный провайдер появится на этапе ETL"
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
-->
|
-->
|
||||||
<remote_servers>
|
<remote_servers>
|
||||||
<clickstream_cluster>
|
<clickstream_cluster>
|
||||||
|
<!--
|
||||||
|
ClickHouse 26.3 передаёт с секретом пользователя-инициатора на
|
||||||
|
соседний шард; секрет сверяется на обеих нодах.
|
||||||
|
-->
|
||||||
|
<secret from_env="CLICKHOUSE_CLUSTER_SECRET"/>
|
||||||
<shard>
|
<shard>
|
||||||
<internal_replication>true</internal_replication>
|
<internal_replication>true</internal_replication>
|
||||||
<replica>
|
<replica>
|
||||||
@@ -33,4 +38,3 @@
|
|||||||
<path>/clickhouse/task_queue/ddl</path>
|
<path>/clickhouse/task_queue/ddl</path>
|
||||||
</distributed_ddl>
|
</distributed_ddl>
|
||||||
</clickhouse>
|
</clickhouse>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<clickhouse>
|
||||||
|
<!--
|
||||||
|
Пользователи и роли объявлены файлом: повторный подъём стенда не хранит
|
||||||
|
доступ в томах. Пароли приходят из .env через Compose.
|
||||||
|
-->
|
||||||
|
<roles>
|
||||||
|
<etl_writer>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW ON default.*</query>
|
||||||
|
<query>GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON stg.*</query>
|
||||||
|
<query>GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON ods.*</query>
|
||||||
|
<query>GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dds.*</query>
|
||||||
|
<query>GRANT SELECT, INSERT, ALTER, CREATE TABLE, CREATE VIEW, DROP TABLE, DROP VIEW, CREATE DATABASE ON dm.*</query>
|
||||||
|
<query>GRANT SELECT ON system.*</query>
|
||||||
|
<query>GRANT CLUSTER ON *.*</query>
|
||||||
|
<query>GRANT REMOTE ON *.*</query>
|
||||||
|
<!-- В 26.3 движок Kafka требует право на внешний источник. -->
|
||||||
|
<query>GRANT KAFKA ON *.*</query>
|
||||||
|
</grants>
|
||||||
|
</etl_writer>
|
||||||
|
<!-- Оба читателя: clickhouse-connect читает system.settings при подключении. -->
|
||||||
|
<bi_reader>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT SELECT ON dds.*</query>
|
||||||
|
<query>GRANT SELECT ON dm.*</query>
|
||||||
|
<query>GRANT SELECT ON system.settings</query>
|
||||||
|
</grants>
|
||||||
|
</bi_reader>
|
||||||
|
<analyst_reader>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT SELECT ON stg.*</query>
|
||||||
|
<query>GRANT SELECT ON ods.*</query>
|
||||||
|
<query>GRANT SELECT ON dds.*</query>
|
||||||
|
<query>GRANT SELECT ON dm.*</query>
|
||||||
|
<query>GRANT SELECT ON system.settings</query>
|
||||||
|
</grants>
|
||||||
|
</analyst_reader>
|
||||||
|
</roles>
|
||||||
|
|
||||||
|
<users>
|
||||||
|
<default>
|
||||||
|
<password from_env="CLICKHOUSE_DEFAULT_PASSWORD"/>
|
||||||
|
</default>
|
||||||
|
<etl>
|
||||||
|
<password from_env="CLICKHOUSE_ETL_PASSWORD"/>
|
||||||
|
<quota>default</quota>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT etl_writer</query>
|
||||||
|
</grants>
|
||||||
|
</etl>
|
||||||
|
<bi>
|
||||||
|
<password from_env="CLICKHOUSE_BI_PASSWORD"/>
|
||||||
|
<quota>default</quota>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT bi_reader</query>
|
||||||
|
</grants>
|
||||||
|
</bi>
|
||||||
|
<analyst>
|
||||||
|
<password from_env="CLICKHOUSE_ANALYST_PASSWORD"/>
|
||||||
|
<quota>default</quota>
|
||||||
|
<grants>
|
||||||
|
<query>GRANT analyst_reader</query>
|
||||||
|
</grants>
|
||||||
|
</analyst>
|
||||||
|
</users>
|
||||||
|
</clickhouse>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
database_name: ClickHouse
|
database_name: ClickHouse
|
||||||
sqlalchemy_uri: clickhousedb://default@clickhouse-02:8123/default
|
sqlalchemy_uri: clickhousedb://bi@clickhouse-02:8123/default
|
||||||
uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d
|
uuid: 4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d
|
||||||
cache_timeout: null
|
cache_timeout: null
|
||||||
expose_in_sqllab: true
|
expose_in_sqllab: true
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import os
|
|||||||
SQLALCHEMY_DATABASE_URI = os.environ["SUPERSET__SQLALCHEMY_DATABASE_URI"]
|
SQLALCHEMY_DATABASE_URI = os.environ["SUPERSET__SQLALCHEMY_DATABASE_URI"]
|
||||||
SECRET_KEY = os.environ["SUPERSET_SECRET_KEY"]
|
SECRET_KEY = os.environ["SUPERSET_SECRET_KEY"]
|
||||||
|
|
||||||
|
|
||||||
|
# Источник данных пока один, поэтому штатному хранилищу паролей Superset
|
||||||
|
# достаточно вернуть один секрет из окружения.
|
||||||
|
def clickhouse_password(_url):
|
||||||
|
return os.environ["CLICKHOUSE_BI_PASSWORD"]
|
||||||
|
|
||||||
|
|
||||||
|
SQLALCHEMY_CUSTOM_PASSWORD_STORE = clickhouse_password
|
||||||
|
|
||||||
SESSION_COOKIE_NAME = "superset_session"
|
SESSION_COOKIE_NAME = "superset_session"
|
||||||
SESSION_COOKIE_SECURE = False
|
SESSION_COOKIE_SECURE = False
|
||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ compose() {
|
|||||||
query() {
|
query() {
|
||||||
local service="$1"
|
local service="$1"
|
||||||
local sql="$2"
|
local sql="$2"
|
||||||
compose exec -T "$service" clickhouse-client --query "$sql" </dev/null
|
compose exec -T "$service" sh -c '
|
||||||
|
clickhouse-client \
|
||||||
|
--user default \
|
||||||
|
--password "$CLICKHOUSE_DEFAULT_PASSWORD" \
|
||||||
|
--query "$1"
|
||||||
|
' _ "$sql" </dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
query_with_timeout() {
|
query_with_timeout() {
|
||||||
@@ -27,7 +32,12 @@ query_with_timeout() {
|
|||||||
local sql="$3"
|
local sql="$3"
|
||||||
timeout --foreground "${timeout_seconds}s" \
|
timeout --foreground "${timeout_seconds}s" \
|
||||||
docker compose --project-directory "$ROOT_DIR" exec -T "$service" \
|
docker compose --project-directory "$ROOT_DIR" exec -T "$service" \
|
||||||
clickhouse-client --query "$sql"
|
sh -c '
|
||||||
|
clickhouse-client \
|
||||||
|
--user default \
|
||||||
|
--password "$CLICKHOUSE_DEFAULT_PASSWORD" \
|
||||||
|
--query "$1"
|
||||||
|
' _ "$sql"
|
||||||
}
|
}
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ check_airflow_probes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_superset() {
|
check_superset() {
|
||||||
|
local clickhouse_password
|
||||||
local config
|
local config
|
||||||
local login
|
local login
|
||||||
local metadata_tables
|
local metadata_tables
|
||||||
@@ -253,6 +254,7 @@ check_superset() {
|
|||||||
config="$(compose config --format json 2>/dev/null || true)"
|
config="$(compose config --format json 2>/dev/null || true)"
|
||||||
user="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_USER // empty' <<<"$config")"
|
user="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_USER // empty' <<<"$config")"
|
||||||
password="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_PASSWORD // empty' <<<"$config")"
|
password="$(jq -r '.services.superset.environment.SUPERSET_ADMIN_PASSWORD // empty' <<<"$config")"
|
||||||
|
clickhouse_password="$(jq -r '.services.superset.environment.CLICKHOUSE_BI_PASSWORD // empty' <<<"$config")"
|
||||||
superset_user="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_USER // empty' <<<"$config")"
|
superset_user="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_USER // empty' <<<"$config")"
|
||||||
superset_password="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_PASSWORD // empty' <<<"$config")"
|
superset_password="$(jq -r '.services["postgres-metadata"].environment.SUPERSET_METADATA_PASSWORD // empty' <<<"$config")"
|
||||||
port="$(published_port superset 8088)"
|
port="$(published_port superset 8088)"
|
||||||
@@ -284,10 +286,10 @@ check_superset() {
|
|||||||
if [[ "$metadata_tables" == '1' ]] && \
|
if [[ "$metadata_tables" == '1' ]] && \
|
||||||
[[ "$metadata_engine" == "postgresql|${superset_user}|postgres-metadata|5432|superset" ]] && \
|
[[ "$metadata_engine" == "postgresql|${superset_user}|postgres-metadata|5432|superset" ]] && \
|
||||||
[[ "$stored_uuid" == '4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d' ]] && \
|
[[ "$stored_uuid" == '4b8f2c6e-1d3a-4f5b-9c7d-2e8a1f0b3c5d' ]] && \
|
||||||
[[ "$stored_uri" == 'clickhousedb://default@clickhouse-02:8123/default' ]]; then
|
[[ "$stored_uri" == "clickhousedb://bi:${clickhouse_password}@clickhouse-02:8123/default" ]]; then
|
||||||
pass 'метаданные Superset живут в Postgres, подготовленное подключение указывает на clickhouse-02'
|
pass 'метаданные Superset живут в Postgres, подготовленное подключение указывает на clickhouse-02'
|
||||||
else
|
else
|
||||||
fail "Superset не подтвердил Postgres и подготовленное подключение к ноде 2: таблицы=${metadata_tables:-нет}, движок=${metadata_engine:-нет}, UUID=${stored_uuid:-нет}, URI=${stored_uri:-нет}"
|
fail "Superset не подтвердил Postgres и подготовленное подключение bi к ноде 2: таблицы=${metadata_tables:-нет}, движок=${metadata_engine:-нет}, UUID=${stored_uuid:-нет}"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ check_grafana_datasource() {
|
|||||||
check_airflow() {
|
check_airflow() {
|
||||||
local airflow_port
|
local airflow_port
|
||||||
local airflow_token
|
local airflow_token
|
||||||
|
local clickhouse_password
|
||||||
local config
|
local config
|
||||||
local connection
|
local connection
|
||||||
local dag
|
local dag
|
||||||
@@ -166,6 +167,7 @@ check_airflow() {
|
|||||||
config="$(compose config --format json 2>/dev/null || true)"
|
config="$(compose config --format json 2>/dev/null || true)"
|
||||||
user="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_USER // empty' <<<"$config")"
|
user="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_USER // empty' <<<"$config")"
|
||||||
password="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_PASSWORD // empty' <<<"$config")"
|
password="$(jq -r '.services["airflow-apiserver"].environment.AIRFLOW_ADMIN_PASSWORD // empty' <<<"$config")"
|
||||||
|
clickhouse_password="$(jq -r '.services["airflow-scheduler"].environment.CLICKHOUSE_ETL_PASSWORD // empty' <<<"$config")"
|
||||||
airflow_port="$(published_port airflow-apiserver 8080)"
|
airflow_port="$(published_port airflow-apiserver 8080)"
|
||||||
|
|
||||||
health="$(curl -sf --max-time 10 \
|
health="$(curl -sf --max-time 10 \
|
||||||
@@ -211,12 +213,13 @@ check_airflow() {
|
|||||||
.connection_id == "clickhouse_default" and
|
.connection_id == "clickhouse_default" and
|
||||||
.host == "clickhouse-01" and
|
.host == "clickhouse-01" and
|
||||||
.port == 8123 and
|
.port == 8123 and
|
||||||
.login == "default" and
|
.login == "etl" and
|
||||||
.schema == "default"
|
.schema == "default"
|
||||||
' >/dev/null 2>&1 <<<"$connection" && \
|
' >/dev/null 2>&1 <<<"$connection" && \
|
||||||
timeout 20s "${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" \
|
timeout 20s "${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" \
|
||||||
exec -T airflow-scheduler \
|
exec -T airflow-scheduler \
|
||||||
curl -sf 'http://clickhouse-01:8123/?query=SELECT%201' \
|
curl -sf -u "etl:${clickhouse_password}" \
|
||||||
|
'http://clickhouse-01:8123/?query=SELECT%201' \
|
||||||
2>/dev/null | grep -qx '1'; then
|
2>/dev/null | grep -qx '1'; then
|
||||||
pass 'подготовленное подключение Airflow указывает на clickhouse-01, нода доступна из контейнера'
|
pass 'подготовленное подключение Airflow указывает на clickhouse-01, нода доступна из контейнера'
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -42,7 +42,12 @@ fail() {
|
|||||||
|
|
||||||
query() {
|
query() {
|
||||||
"${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" exec -T clickhouse-01 \
|
"${COMPOSE_CMD[@]}" --project-directory "$ROOT_DIR" exec -T clickhouse-01 \
|
||||||
clickhouse-client --query "$1" </dev/null
|
sh -c '
|
||||||
|
clickhouse-client \
|
||||||
|
--user default \
|
||||||
|
--password "$CLICKHOUSE_DEFAULT_PASSWORD" \
|
||||||
|
--query "$1"
|
||||||
|
' _ "$1" </dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
expected="$(jq '[.days[].events] | add' "$INVENTORY")"
|
expected="$(jq '[.days[].events] | add' "$INVENTORY")"
|
||||||
|
|||||||
Reference in New Issue
Block a user