Паттерны компоновки DE-стека
В прошлых четырёх уроках мы поднимали изолированные стенды: Airflow отдельно, Kafka отдельно, Spark отдельно, ClickHouse отдельно. На практике DE работает со всеми этими сервисами одновременно: Airflow дёргает Spark, Spark пишет в ClickHouse, ClickHouse подписан на Kafka. Один стек.
Этот урок — про паттерны компоновки: как соединить компоненты в один проект без хаоса в YAML. Темы: общая сеть, healthcheck-driven startup, secrets через .env, multi-file compose и profiles для опциональных сервисов.
Switches vs Hubs — как коммутатор знает куда слать фрейм
Паттерн 1: общая Docker-сеть
Самый частый вопрос junior’а: “Как Airflow подключится к моему Postgres?”. Ответ — общая сеть.
В compose есть три уровня сетей:
- Default — compose автоматически создаёт сеть
{project}_default, если ты не указал networks. Все сервисы там. - Named in-project — ты создаёшь
networks: { de-net: {} }и привязываешь сервисы. Изоляция от других compose-проектов. - External — сеть, созданная вне compose (
docker network create de-shared). Несколько compose-проектов могут к ней подключиться.
Для DE-стека правильный выбор — named сеть в одном compose, или external для multi-file подхода.
# Опция А: всё в одном compose.yml, одна named-сеть
networks:
de-net:
driver: bridge
services:
postgres: { networks: [de-net] }
airflow: { networks: [de-net] }
kafka: { networks: [de-net] }
clickhouse: { networks: [de-net] }
# Опция Б: разные compose-файлы делят external-сеть
# Сначала создать руками:
# docker network create de-shared
# В compose.airflow.yml:
networks:
default:
name: de-shared
external: true
# В compose.kafka.yml:
networks:
default:
name: de-shared
external: true
Паттерн 2: healthcheck-driven startup
Один из самых раздражающих junior-багов: “у меня Airflow упал при старте, потому что Postgres ещё не успел подняться”. Решается через healthcheck + depends_on с conditions.
Compose v2 умеет:
service_started— стартовал процесс (быстрый, ненадёжный).service_healthy— healthcheck вернул успех.service_completed_successfully— контейнер завершился с exit 0 (для init-job’ов).
services:
postgres:
image: postgres:16
healthcheck:
test: ["CMD", "pg_isready", "-U", "airflow"]
interval: 5s
retries: 10
airflow-init:
image: apache/airflow:2.10.3
depends_on:
postgres:
condition: service_healthy # ждёт, пока pg_isready
command: airflow db migrate
airflow-webserver:
image: apache/airflow:2.10.3
depends_on:
airflow-init:
condition: service_completed_successfully # ждёт exit 0
Без healthcheck’ов depends_on гарантирует только порядок старта контейнеров, но не готовность сервиса. Postgres может запустить postgres процесс, но ещё не принимать соединения. Airflow попробует подключиться и упадёт.
Правило: для каждого критичного сервиса (БД, message broker, key-value store) пиши healthcheck. Это 3 строчки YAML, которые экономят часы debug’а.
Паттерн 3: secrets через .env
Никогда не пиши пароли в compose.yml. Используй .env:
# .env (в gitignore!)
POSTGRES_PASSWORD=airflow-secret-2026
AIRFLOW_FERNET_KEY=fa6sFvHkjhdGFsa8jdhgFKJhg23dfgKJh==
KAFKA_BOOTSTRAP=kafka:9092
CLICKHOUSE_PASSWORD=ch-strong-pass
# compose.yml
services:
postgres:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
airflow-webserver:
environment:
AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW_FERNET_KEY}
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql://airflow:${POSTGRES_PASSWORD}@postgres/airflow
Compose автоматически подхватывает .env из директории, где запускается docker compose. Переменные подставляются при парсинге YAML.
ВНИМАНИЕ: добавь .env в .gitignore СРАЗУ при создании репо. Если случайно закоммитил пароль — git rm и переустановить пароль (старый уже скомпрометирован, история git публична). Создай .env.example с пустыми значениями для документации.
Для production-секретов (не dev) есть secrets: в compose v3 — это файлы, которые монтируются как /run/secrets/{name} внутри контейнера. Но для compose-стенда .env обычно достаточно.
Паттерн 4: multi-file compose
Когда compose.yml перевалил за 300 строк — разбивай. Compose поддерживает множество файлов:
docker compose -f compose.yml -f compose.kafka.yml -f compose.observability.yml up -d
Структура репо:
project/
├── docker/
│ ├── compose.yml # ядро: airflow, postgres
│ ├── compose.kafka.yml # kafka + kafka-ui
│ ├── compose.spark.yml # spark-master + workers
│ ├── compose.clickhouse.yml # clickhouse
│ └── compose.observability.yml # prometheus + grafana
├── .env
├── dags/
├── data/
└── Makefile
Compose сливает все указанные файлы в один effective compose. Service с одинаковым именем мерджится по правилам YAML (последний выигрывает для scalars, list/dict — пополняется).
Удобно сделать Makefile-аналог:
COMPOSE_FILES = -f compose.yml -f compose.kafka.yml -f compose.clickhouse.yml
up:
docker compose ${COMPOSE_FILES} up -d
down:
docker compose ${COMPOSE_FILES} down
logs:
docker compose ${COMPOSE_FILES} logs -f
И теперь просто make up / make down / make logs.
Паттерн 5: profiles для опциональных сервисов
В одном compose-файле можно описать профили — группы сервисов, которые стартуют только при явном опт-ине. Идеально для опциональных сервисов: spark-cluster нужен не каждый день, observability — для продвинутого debug.
services:
postgres:
image: postgres:16
# без profile -- всегда поднимается
airflow-webserver:
image: apache/airflow:2.10.3
# без profile -- всегда поднимается
spark-master:
image: bitnami/spark:3.5
profiles: ["spark"] # только если --profile spark
spark-worker-1:
image: bitnami/spark:3.5
profiles: ["spark"]
prometheus:
image: prom/prometheus
profiles: ["observability"]
grafana:
image: grafana/grafana
profiles: ["observability"]
Запуск:
# Только ядро
docker compose up -d
# postgres + airflow
# С Spark
docker compose --profile spark up -d
# postgres + airflow + spark-master + spark-worker
# Со всем
docker compose --profile spark --profile observability up -d
# Несколько профилей через переменную окружения
COMPOSE_PROFILES=spark,observability docker compose up -d
Профили — киллер-фича для большого стека. У тебя один compose.yml на ~500 строк, но ты включаешь только нужные слои. Если работаешь над DAG’ом, который не трогает Spark — не поднимай 2 worker’а, экономь RAM и CPU.
Куда складывать compose-файлы в репо
Стандартная convention:
project/
├── docker/ # ВСЕ compose-файлы и Dockerfile'ы тут
│ ├── compose.yml
│ ├── compose.spark.yml
│ ├── airflow/
│ │ └── Dockerfile # Custom Airflow image (с твоими providers)
│ └── producer/
│ └── Dockerfile
├── dags/ # Airflow DAG-и
├── notebooks/ # Jupyter (если нужны)
├── sql/ # SQL-миграции, init-скрипты
├── tests/ # pytest-тесты
├── .env.example # пример .env (без секретов)
├── .env # реальный .env (в .gitignore)
└── Makefile # обёртки над docker compose
Почему docker/:
- Отделяет инфраструктуру от кода приложения.
- Один взгляд — и понятно, где compose, где Dockerfile’ы.
- Можно делать relative path в compose:
build: ./airflow(соседняя директория).
В крупных проектах часто появляется infra/ или deploy/ — то же самое, просто другое имя.
Локальный override (.override.yml)
Compose автоматически подхватывает compose.override.yml, если он есть в той же директории. Это идеально для локальных кастомизаций, которые не должны попасть в git:
# compose.override.yml (в .gitignore)
services:
airflow-webserver:
ports:
- "8081:8080" # у меня 8080 занят другим проектом, мап на 8081
postgres:
ports:
- "5433:5432" # хочу подключиться pgAdmin'ом с хоста
docker compose up без флагов сольёт compose.yml + compose.override.yml. Команда docker compose -f compose.yml up (явный -f) НЕ подхватит override — только базовый.
Минимальный шаблон DE-стенда
Соберём всё в один компактный пример:
# compose.yml
networks:
de-net:
driver: bridge
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: airflow
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: airflow
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "airflow"]
interval: 5s
networks: [de-net]
airflow-webserver:
image: apache/airflow:2.10.3
environment:
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql://airflow:${POSTGRES_PASSWORD}@postgres/airflow
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
networks: [de-net]
command: webserver
kafka:
image: bitnami/kafka:3.8
profiles: ["streaming"]
environment:
KAFKA_CFG_NODE_ID: 1
KAFKA_CFG_PROCESS_ROLES: controller,broker
# ... остальные KRaft-настройки
networks: [de-net]
clickhouse:
image: clickhouse/clickhouse-server:24.10
profiles: ["streaming"]
ulimits:
nofile: { soft: 262144, hard: 262144 }
networks: [de-net]
volumes:
pg-data:
И workflow:
# Базовое: только Airflow + Postgres
docker compose up -d
# Стриминговый стек: добавляются Kafka + ClickHouse
docker compose --profile streaming up -d
# Stop streaming, оставить Airflow
docker compose --profile streaming down
docker compose up -d # Airflow остался live
Попробуй сам
# 1. Создай .env с твоими паролями
cat > .env <<EOF
POSTGRES_PASSWORD=secret123
CLICKHOUSE_PASSWORD=ch-pass
EOF
# 2. Скопируй шаблон выше в compose.yml
# 3. Базовый запуск
docker compose up -d
docker compose ps # postgres + airflow
# 4. Запуск со streaming profile
docker compose --profile streaming up -d
docker compose ps # + kafka + clickhouse
# 5. Создай compose.override.yml для локальной кастомизации:
cat > compose.override.yml <<EOF
services:
airflow-webserver:
ports:
- "8088:8080"
EOF
docker compose up -d # webserver теперь на :8088
# 6. Multi-file (для теста)
docker compose -f compose.yml -f compose.override.yml config
# Покажет effective compose
# 7. Cleanup
docker compose --profile streaming down -v