iac/docker-compose.yaml

886 lines
43 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Общая конфигурация воркер-нод k3s — см. сервисы k3s-worker-1..3.
# У каждой ноды СВОЙ том состояния и СВОЙ каталог PVC-хранилища: провижинер
# local-path кладёт данные на ту ноду, где под запустился первым, и через
# nodeAffinity навсегда привязывает к ней PV.
x-k3s-worker: &k3s-worker
image: ${K3S_IMAGE:-rancher/k3s:v1.36.2-k3s1}
command: ["agent"]
privileged: true
cgroup: host # kubelet в cgroup v2 (Ubuntu 22.04+/RedOS)
restart: unless-stopped
depends_on:
k3s-server:
condition: service_healthy
environment:
K3S_URL: https://k3s-server:6443
K3S_TOKEN: ${K3S_TOKEN:-supersecrettoken}
# ВНИМАНИЕ: volumes здесь не задаём — merge-ключ `<<:` не сливает списки,
# и volumes сервиса перекрыл бы якорь целиком. Монтирования, включая
# registries.yaml, перечислены в каждом воркере явно.
tmpfs:
- /run
- /var/run
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
services:
k3s-server:
image: ${K3S_IMAGE:-rancher/k3s:v1.36.2-k3s1}
container_name: k3s-server
hostname: k3s-server # имя ноды в кластере — см. комментарий у воркеров
command:
- server
- --tls-san=127.0.0.1
- --tls-san=k3s-server
- --tls-san=${K3S_API_HOST:-127.0.0.1} # внешний IP/домен для доступа к apiserver извне
- --disable=traefik
privileged: true
cgroup: host # kubelet в cgroup v2 (Ubuntu 22.04+/RedOS)
restart: unless-stopped
environment:
K3S_TOKEN: ${K3S_TOKEN:-supersecrettoken}
K3S_KUBECONFIG_OUTPUT: /output/kubeconfig.yaml
K3S_KUBECONFIG_MODE: "644"
#ports:
# - "6443:6443" # kube-apiserver — доступен извне; ОБЯЗАТЕЛЬНО ограничь фаерволом до доверенных IP
# - "80:80" # ingress http — задействуются после установки ingress-контроллера
# - "443:443" # ingress https
volumes:
- k3s-server-data:/var/lib/rancher/k3s # etcd/state, образы, манифесты — только named volume
- ./k3s:/output:z # отсюда забираем контекст k8s (:z — SELinux label для RedOS)
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z # доступ containerd к cr.yandex
# PVC-данные local-path-provisioner (StorageClass local-path, он же default
# в k3s). Перекрываем ровно дефолтный путь провижинера, поэтому настраивать
# ConfigMap local-path-config не нужно. Bind-mount на хост означает, что
# файлы БД/очередей/объектов переживают пересоздание контейнера k3s и
# доступны для бэкапа обычными средствами хоста.
# Каталог намеренно не вложен в ./k3s: тот смонтирован ещё и как /output,
# и данные PVC засветились бы там же. У каждой ноды свой подкаталог, иначе
# сервер видел бы каталоги воркеров как посторонние PV в своём storage.
- ./k3s-storage/server:/var/lib/rancher/k3s/storage:z
tmpfs:
- /run
- /var/run
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
healthcheck:
test: ["CMD", "kubectl", "get", "--raw=/readyz"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
# hostname задаёт ИМЯ НОДЫ в кластере (k3s берёт его из hostname). Без него
# нодой становится ID контейнера, а он меняется при пересоздании — и тогда
# local-path PV, привязанные к старому имени через nodeAffinity, «повисают».
k3s-worker-1:
<<: *k3s-worker
container_name: k3s-worker-1
hostname: k3s-worker-1
volumes:
- k3s-worker-1-data:/var/lib/rancher/k3s
- ./k3s-storage/worker-1:/var/lib/rancher/k3s/storage:z
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z
k3s-worker-2:
<<: *k3s-worker
container_name: k3s-worker-2
hostname: k3s-worker-2
volumes:
- k3s-worker-2-data:/var/lib/rancher/k3s
- ./k3s-storage/worker-2:/var/lib/rancher/k3s/storage:z
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z
k3s-worker-3:
<<: *k3s-worker
container_name: k3s-worker-3
hostname: k3s-worker-3
volumes:
- k3s-worker-3-data:/var/lib/rancher/k3s
- ./k3s-storage/worker-3:/var/lib/rancher/k3s/storage:z
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z
# Одноразовый провижининг k3s: namespace processing + DNS-мост postgres/minio.
# Образ k3s содержит kubectl; server/tls перекрываем на k3s-server:6443
# (kubeconfig из /output даёт клиентские креды, адрес/TLS переопределяем флагами).
processing-k8s-init:
image: ${K3S_IMAGE:-rancher/k3s:v1.36.2-k3s1}
container_name: processing-k8s-init
restart: "no"
depends_on:
k3s-server:
condition: service_healthy
entrypoint: ["/bin/sh", "-ec"]
command:
- |
kubectl --kubeconfig /kube/config \
--server https://k3s-server:6443 --insecure-skip-tls-verify \
apply -f /manifests/
volumes:
- ./k3s/kubeconfig.yaml:/kube/config:ro,z
- ./k3s/manifests/processing:/manifests:ro,z
# ---------------------------------------------------------------------------
# GitOps-подложка контура: gitea (источник) + vault (секреты) + flux (в k3s).
# Порядок: gitea → gitea-init (админ/орг/репо) → flux-bootstrap (ставит Flux
# в k3s и привязывает его к репозиторию в gitea).
# ---------------------------------------------------------------------------
gitea:
image: ${GITEA_IMAGE:-gitea/gitea:1.22}
container_name: gitea
restart: unless-stopped
environment:
USER_UID: 1000
USER_GID: 1000
GITEA__server__ROOT_URL: ${GITEA_ROOT_URL:-http://localhost:3000/}
GITEA__server__SSH_PORT: ${GITEA_SSH_PORT:-2222}
# Пропускаем web-визард установки: конфигурация задаётся здесь, админа
# заводит gitea-init. Без INSTALL_LOCK первый запрос уводит на /install.
GITEA__security__INSTALL_LOCK: "true"
GITEA__database__DB_TYPE: sqlite3
GITEA__service__DISABLE_REGISTRATION: "true"
ports:
- "3000:3000" # web UI (80/443 заняты k3s)
- "${GITEA_SSH_PORT:-2222}:22" # git over ssh
volumes:
- ./gitea-data:/data:z # :z — SELinux label для RedOS
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/healthz"]
interval: 10s
timeout: 5s
retries: 12
start_period: 20s
# Статический IP по образцу postgres/minio. Он обязателен: source-controller
# Flux работает ВНУТРИ k3s, где своя DNS (CoreDNS), и имя compose-сети
# `gitea` там не резолвится. Поды k3s дотягиваются до 172.28.0.0/16 через
# SNAT на ноде, поэтому в URL источника используется именно этот адрес.
networks:
default:
ipv4_address: 172.28.0.12
# Одноразовый провижининг gitea: администратор + организация + пустой репозиторий.
# Делит /data с gitea (CLI пишет в ту же sqlite), поэтому запускается тем же uid.
# Все шаги идемпотентны: повторный прогон на готовой инсталляции — no-op.
gitea-init:
image: ${GITEA_IMAGE:-gitea/gitea:1.22}
container_name: gitea-init
restart: "no"
user: "1000:1000"
depends_on:
gitea:
condition: service_healthy
environment:
GITEA_ADMIN_USER: ${GITEA_ADMIN_USER:-sarex}
GITEA_ADMIN_PASSWORD: ${GITEA_ADMIN_PASSWORD:?GITEA_ADMIN_PASSWORD is required}
GITEA_ADMIN_EMAIL: ${GITEA_ADMIN_EMAIL:-sarex@sarex.local}
GITEA_ORG: ${GITEA_ORG:-infra}
GITEA_REPO: ${GITEA_REPO:-iac}
volumes:
- ./gitea-data:/data:z
entrypoint: ["/bin/sh", "-ec"]
command:
- |
export GITEA_WORK_DIR=/data/gitea
CONF=/data/gitea/conf/app.ini
# 1. Администратор. Существующий пользователь → ненулевой код, это норма.
gitea --config "$$CONF" admin user create \
--admin --username "$$GITEA_ADMIN_USER" --password "$$GITEA_ADMIN_PASSWORD" \
--email "$$GITEA_ADMIN_EMAIL" --must-change-password=false 2>&1 \
| grep -v 'already exists' || true
API=http://gitea:3000/api/v1
AUTH="$$GITEA_ADMIN_USER:$$GITEA_ADMIN_PASSWORD"
# 2. Организация. 422 = уже существует.
curl -fsS -u "$$AUTH" -X POST "$$API/orgs" \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$$GITEA_ORG\"}" >/dev/null 2>&1 || true
# 3. Репозиторий. auto_init=false — первый коммит сделает flux bootstrap.
curl -fsS -u "$$AUTH" -X POST "$$API/orgs/$$GITEA_ORG/repos" \
-H 'Content-Type: application/json' \
-d "{\"name\":\"$$GITEA_REPO\",\"private\":true,\"auto_init\":false}" \
>/dev/null 2>&1 || true
# Проверяем, что репозиторий действительно доступен — иначе flux bootstrap
# упадёт позже и менее внятно.
curl -fsS -u "$$AUTH" "$$API/repos/$$GITEA_ORG/$$GITEA_REPO" >/dev/null
echo "gitea-init: $$GITEA_ORG/$$GITEA_REPO готов"
vault:
image: ${VAULT_IMAGE:-hashicorp/vault:1.18}
container_name: vault
restart: unless-stopped
# Только "server", без -config: entrypoint образа сам добавляет
# -config=/vault/config. Явный путь к файлу внутри этого же каталога
# приводит к двойной загрузке конфига и падению на
# "listen tcp4 0.0.0.0:8200: bind: address already in use".
command: ["server"]
cap_add:
- IPC_LOCK
environment:
VAULT_ADDR: http://127.0.0.1:8200
volumes:
# :ro осознанно — entrypoint пишет в лог безвредные "chown: Read-only
# file system", но конфиг остаётся неизменяемым изнутри контейнера.
- ./vault/config:/vault/config:ro,z
- sarex-vault-data:/vault/file
- sarex-vault-init:/vault/init
healthcheck:
# /v1/sys/health отдаёт 200 только когда Vault инициализирован и распечатан
# (501 — не инициализирован, 503 — запечатан). Значит healthy здесь
# означает «готов принимать секреты», и на это можно вешать depends_on.
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8200/v1/sys/health"]
interval: 10s
timeout: 5s
retries: 30
start_period: 10s
# Инициализация и авто-распечатывание Vault. Живёт постоянно: после рестарта
# хоста Vault поднимается запечатанным, и цикл распечатывает его сам.
# Ключ и root-токен лежат на отдельном volume (sarex-vault-init), не в git.
vault-init:
image: ${VAULT_IMAGE:-hashicorp/vault:1.18}
container_name: vault-init
restart: unless-stopped
depends_on:
vault:
condition: service_started
environment:
VAULT_ADDR: http://vault:8200
volumes:
- sarex-vault-init:/vault/init
entrypoint: ["/bin/sh", "-ec"]
command:
- |
UNSEAL=/vault/init/unseal.key
ROOT=/vault/init/root.token
while :; do
# `vault operator init -status`: 0 — инициализирован, 2 — нет,
# прочее — Vault ещё не отвечает. Под `set -e` ненулевой код нужно
# перехватить явно, иначе скрипт завершится.
rc=0
vault operator init -status >/dev/null 2>&1 || rc=$$?
if [ "$$rc" -eq 0 ]; then
# Инициализирован. `vault status`: 0 — распечатан, 2 — запечатан.
if ! vault status >/dev/null 2>&1; then
if [ -s "$$UNSEAL" ]; then
vault operator unseal "$$(cat $$UNSEAL)" >/dev/null \
&& echo "vault-init: распечатан"
else
echo "vault-init: Vault запечатан, но $$UNSEAL отсутствует —" \
"распечатать нечем (том sarex-vault-init потерян?)" >&2
fi
fi
elif [ "$$rc" -eq 2 ]; then
# Отвечает, но не инициализирован — инициализируем одним ключом.
# Текстовый вывод парсим awk'ом: jq в образе нет.
if vault operator init -key-shares=1 -key-threshold=1 > /vault/init/init.txt 2>/dev/null; then
awk '/^Unseal Key 1:/{print $$4}' /vault/init/init.txt > "$$UNSEAL"
awk '/^Initial Root Token:/{print $$4}' /vault/init/init.txt > "$$ROOT"
chmod 600 "$$UNSEAL" "$$ROOT" /vault/init/init.txt
vault operator unseal "$$(cat $$UNSEAL)" >/dev/null
echo "vault-init: инициализирован и распечатан"
# kv-v2 на пути secrets/ — так же, как в k8s-контурах
# (манифесты ссылаются на secrets/data/...).
VAULT_TOKEN="$$(cat $$ROOT)" vault secrets enable -path=secrets kv-v2 \
>/dev/null 2>&1 && echo "vault-init: включён kv-v2 на secrets/"
fi
fi
sleep 10
done
# Одноразовый провижининг namespace flux-system и DNS-моста до gitea.
# Должен отработать ДО flux-bootstrap: тот дожидается готовности GitRepository,
# а она невозможна, пока source-controller не может отрезолвить источник.
flux-k8s-init:
image: ${K3S_IMAGE:-rancher/k3s:v1.36.2-k3s1}
container_name: flux-k8s-init
restart: "no"
depends_on:
k3s-server:
condition: service_healthy
environment:
GITEA_BRIDGE_IP: ${GITEA_BRIDGE_IP:-172.28.0.12}
volumes:
- ./k3s/kubeconfig.yaml:/kube/config:ro,z
- ./k3s/manifests/flux:/manifests:ro,z
entrypoint: ["/bin/sh", "-ec"]
command:
- |
sed "s|__GITEA_BRIDGE_IP__|$$GITEA_BRIDGE_IP|g" /manifests/bridge-gitea.yaml \
| kubectl --kubeconfig /kube/config \
--server https://k3s-server:6443 --insecure-skip-tls-verify \
apply -f -
# Одноразовый bootstrap FluxCD внутрь k3s с привязкой к репозиторию в gitea.
# Flux — это контроллеры Kubernetes, в compose живёт только этот установщик.
# После успешного прогона источником правды становится gitea, а не compose.
flux-bootstrap:
image: ${FLUX_CLI_IMAGE:-ghcr.io/fluxcd/flux-cli:v2.8.5}
container_name: flux-bootstrap
restart: "no"
depends_on:
k3s-server:
condition: service_healthy
gitea-init:
condition: service_completed_successfully
flux-k8s-init:
condition: service_completed_successfully
# Один и тот же URL резолвится в двух разных контекстах: внутри k3s — через
# Service флакс-моста, здесь, в compose-сети, — через эту запись в /etc/hosts.
# Без неё flux CLI не смог бы запушить манифесты в репозиторий.
extra_hosts:
- "gitea.flux-system.svc.cluster.local:${GITEA_BRIDGE_IP:-172.28.0.12}"
environment:
GITEA_ADMIN_USER: ${GITEA_ADMIN_USER:-sarex}
GITEA_ADMIN_PASSWORD: ${GITEA_ADMIN_PASSWORD:?GITEA_ADMIN_PASSWORD is required}
GITEA_ORG: ${GITEA_ORG:-infra}
GITEA_REPO: ${GITEA_REPO:-iac}
AERO_GIT_BRANCH: ${AERO_GIT_BRANCH:-master}
AERO_FLUX_PATH: ${AERO_FLUX_PATH:-clusters/aero}
# Должен совпадать с ipv4_address сервиса gitea.
GITEA_BRIDGE_IP: ${GITEA_BRIDGE_IP:-172.28.0.12}
volumes:
- ./k3s/kubeconfig.yaml:/kube/config:ro,z
entrypoint: ["/bin/sh", "-ec"]
command:
- |
# k3s пишет kubeconfig с server: https://127.0.0.1:6443 — изнутри другого
# контейнера это неверный адрес. Подменяем на имя сервиса; сертификат
# apiserver уже содержит --tls-san=k3s-server, поэтому TLS проверяется
# штатно, без --insecure.
sed 's#https://127.0.0.1:6443#https://k3s-server:6443#' /kube/config > /tmp/kubeconfig
export KUBECONFIG=/tmp/kubeconfig
# Адрес источника — имя Service'а моста (flux-k8s-init), а не IP: так
# адрес gitea задан ровно в одном месте — переменной GITEA_BRIDGE_IP.
flux bootstrap git \
--url="http://gitea.flux-system.svc.cluster.local:3000/$$GITEA_ORG/$$GITEA_REPO.git" \
--branch="$$AERO_GIT_BRANCH" \
--path="$$AERO_FLUX_PATH" \
--username="$$GITEA_ADMIN_USER" \
--password="$$GITEA_ADMIN_PASSWORD" \
--token-auth \
--allow-insecure-http
# ---------------------------------------------------------------------------
# Инфраструктурные зависимости apps/django (sarex-backend).
# Django деплоится в k3s и ходит сюда — про связность k3s→compose см. README.
# Креды/версии взяты из sarex-backend/docker-compose.yaml и apps/django/.env.example.
# ---------------------------------------------------------------------------
postgres:
image: ${POSTGRES_IMAGE:-postgres:12-alpine}
container_name: postgres
restart: unless-stopped
environment:
# суперпользователь (администрирование)
POSTGRES_DB: ${SAREX_POSTGRES_DB:-sarex_db}
POSTGRES_USER: ${SAREX_POSTGRES_USER:-sarex}
POSTGRES_PASSWORD: ${SAREX_POSTGRES_PASSWORD:-sarex}
# роли приложений с паролями — читаются init-скриптом 00-init.sh
DJANGO_DB: ${SAREX_POSTGRES_DB:-sarex_db}
DJANGO_DB_USER: ${SAREX_DJANGO_DB_USER:-django}
DJANGO_DB_PASSWORD: ${SAREX_DJANGO_DB_PASSWORD:-django-secret}
PROC_DB: ${SAREX_PROCESSING_DB:-workflow_db}
PROC_DB_USER: ${SAREX_PROCESSING_DB_USER:-processing}
PROC_DB_PASSWORD: ${SAREX_PROCESSING_DB_PASSWORD:-processing-secret}
volumes:
- sarex-postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${SAREX_POSTGRES_USER:-sarex} -d ${SAREX_POSTGRES_DB:-sarex_db}"]
interval: 10s
timeout: 5s
retries: 12
start_period: 10s
# Статический IP: на него ссылается k8s-Endpoints `postgres` (DNS-мост в k3s).
networks:
default:
ipv4_address: 172.28.0.10
# Одноразовый init: на каждое приложение заводит роль (LOGIN+пароль) и базу
# данных с этим владельцем. Идемпотентно (по образцу minio-init).
postgres-init:
image: ${POSTGRES_IMAGE:-postgres:12-alpine}
depends_on:
postgres:
condition: service_healthy
entrypoint:
- /bin/sh
- -c
- |
set -e
until psql -h postgres -U "$$SUPER_USER" -d "$$SUPER_DB" -c 'SELECT 1' >/dev/null 2>&1; do
echo 'waiting for postgres...'; sleep 2;
done
q() { psql -v ON_ERROR_STOP=1 -h postgres -U "$$SUPER_USER" -d "$$SUPER_DB" "$$@"; }
# provision <db> <user> <password>
provision() {
q -tc "SELECT 1 FROM pg_roles WHERE rolname='$$2'" | grep -q 1 \
|| q -c "CREATE ROLE \"$$2\" LOGIN PASSWORD '$$3'"
q -c "ALTER ROLE \"$$2\" WITH LOGIN PASSWORD '$$3'"
q -tc "SELECT 1 FROM pg_database WHERE datname='$$1'" | grep -q 1 \
|| q -c "CREATE DATABASE \"$$1\" OWNER \"$$2\""
q -c "ALTER DATABASE \"$$1\" OWNER TO \"$$2\""
q -c "GRANT ALL PRIVILEGES ON DATABASE \"$$1\" TO \"$$2\""
echo "db $$1 + user $$2 ready"
}
provision "$$DJANGO_DB" "$$DJANGO_USER" "$$DJANGO_PASSWORD"
provision "$$PROC_DB" "$$PROC_USER" "$$PROC_PASSWORD"
provision "$$BIM_DB" "$$BIM_USER" "$$BIM_PASSWORD"
provision "$$WORKSPACE_DB" "$$WORKSPACE_USER" "$$WORKSPACE_PASSWORD"
# Расширения PostgreSQL в базе django: CREATE EXTENSION может только
# суперпользователь. Django-миграции (CreateExtension) идут с IF NOT
# EXISTS → после этого станут no-op и не упадут на правах.
for ext in $$DJANGO_EXTENSIONS; do
q -c "CREATE EXTENSION IF NOT EXISTS \"$$ext\"" && echo "extension $$ext ready in $$DJANGO_DB"
done
# Расширения для processing (workflow_db) — ОТДЕЛЬНАЯ база, поэтому явно
# -d PROC_DB. Роль processing не суперпользователь → CREATE EXTENSION в
# её миграциях (uuid-ossp/ltree/hstore) падает; заводим суперпользователем.
for ext in $$PROC_EXTENSIONS; do
psql -v ON_ERROR_STOP=1 -h postgres -U "$$SUPER_USER" -d "$$PROC_DB" \
-c "CREATE EXTENSION IF NOT EXISTS \"$$ext\"" && echo "extension $$ext ready in $$PROC_DB"
done
environment:
PGPASSWORD: ${SAREX_POSTGRES_PASSWORD:-sarex}
SUPER_USER: ${SAREX_POSTGRES_USER:-sarex}
SUPER_DB: ${SAREX_POSTGRES_DB:-sarex_db}
# django (использует основную базу SAREX_POSTGRES_DB)
DJANGO_DB: ${SAREX_POSTGRES_DB:-sarex_db}
DJANGO_USER: ${SAREX_DJANGO_DB_USER:-django}
DJANGO_PASSWORD: ${SAREX_DJANGO_DB_PASSWORD:-django-secret}
DJANGO_EXTENSIONS: ${SAREX_DJANGO_DB_EXTENSIONS:-ltree}
# processing
PROC_DB: ${SAREX_PROCESSING_DB:-workflow_db}
PROC_USER: ${SAREX_PROCESSING_DB_USER:-processing}
PROC_PASSWORD: ${SAREX_PROCESSING_DB_PASSWORD:-processing-secret}
PROC_EXTENSIONS: ${SAREX_PROCESSING_DB_EXTENSIONS:-uuid-ossp ltree hstore}
# bim
BIM_DB: ${SAREX_BIM_DB:-bim_db}
BIM_USER: ${SAREX_BIM_DB_USER:-bim}
BIM_PASSWORD: ${SAREX_BIM_DB_PASSWORD:-bim-secret}
# workspace
WORKSPACE_DB: ${SAREX_WORKSPACE_DB:-workspace_db}
WORKSPACE_USER: ${SAREX_WORKSPACE_DB_USER:-workspace}
WORKSPACE_PASSWORD: ${SAREX_WORKSPACE_DB_PASSWORD:-workspace-secret}
restart: "no"
redis:
image: ${SAREX_REDIS_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/redis:7.0.10}
container_name: redis
restart: unless-stopped
# protected-mode no: redis только во внутренней docker-сети (порт наружу не
# публикуется), иначе бэкенд/celery из других контейнеров отбиваются "DENIED"
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning", "--protected-mode", "no", "--stop-writes-on-bgsave-error", "no"]
volumes:
- sarex-redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 12
start_period: 5s
rabbitmq:
image: ${SAREX_RABBITMQ_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/rabbimtq:3.11.23}
container_name: rabbitmq
restart: unless-stopped
environment:
RABBITMQ_VHOST: ${SAREX_RABBITMQ_VHOST:-api}
RABBITMQ_USERNAME: ${SAREX_RABBITMQ_USER:-rabbit}
RABBITMQ_PASSWORD: ${SAREX_RABBITMQ_PASSWORD:-rabbit}
RABBITMQ_DISK_FREE_ABSOLUTE_LIMIT: 200MB
volumes:
- sarex-rabbitmq-data:/bitnami/rabbitmq/mnesia
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 15s
timeout: 10s
retries: 12
start_period: 30s
minio:
image: ${SAREX_MINIO_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/minio:250724}
container_name: minio
restart: unless-stopped
command: ["server", "/data", "--console-address", ":9001"]
environment:
MINIO_ROOT_USER: ${SAREX_MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${SAREX_MINIO_ROOT_PASSWORD:-minioadmin}
# Консоль отдаётся через верхний nginx с корня minio-домена
MINIO_BROWSER_REDIRECT_URL: ${SAREX_MINIO_CONSOLE_URL:-https://minio.sarex.local}
volumes:
- sarex-minio-data:/data
# Статический IP: на него ссылается k8s-Endpoints `minio` (DNS-мост в k3s).
networks:
default:
ipv4_address: 172.28.0.11
# Одноразовый init: ждёт MinIO и на каждое приложение заводит бакет +
# отдельного пользователя со scoped-политикой (доступ только к своему бакету).
minio-init:
image: ${SAREX_MC_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/mc:250724}
depends_on:
- minio
entrypoint:
- /bin/sh
- -c
- |
until mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD"; do
echo 'waiting for minio...'; sleep 2;
done
# provision <bucket> <user> <password> <policy>
provision() {
mc mb -p "local/$$1"
mc admin user add local "$$2" "$$3"
printf '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:*"],"Resource":["arn:aws:s3:::%s","arn:aws:s3:::%s/*"]}]}' "$$1" "$$1" > /tmp/policy.json
mc admin policy create local "$$4" /tmp/policy.json || true
mc admin policy attach local "$$4" --user "$$2" || true
echo "bucket $$1 + user $$2 ready"
}
# Единый бакет на все приложения (processing/measurements тоже сюда)
provision "$$MINIO_BUCKET" "$$MINIO_APP_USER" "$$MINIO_APP_PASSWORD" "$$MINIO_APP_POLICY"
environment:
MINIO_ROOT_USER: ${SAREX_MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${SAREX_MINIO_ROOT_PASSWORD:-minioadmin}
# единый бакет/пользователь приложения
MINIO_BUCKET: ${SAREX_MINIO_BUCKET:-sarex-media-storage}
MINIO_APP_USER: ${SAREX_MINIO_APP_USER:-sarex-app}
MINIO_APP_PASSWORD: ${SAREX_MINIO_APP_PASSWORD:-sarex-app-secret}
MINIO_APP_POLICY: ${SAREX_MINIO_APP_POLICY:-sarex-app-rw}
restart: "no"
# ---------------------------------------------------------------------------
# Приложение sarex (backend + celery + frontend).
# Образы приватные (cr.yandex) — нужен `docker login cr.yandex`.
# Для локальной сборки backend раскомментируй build (репозиторий — соседний).
# ---------------------------------------------------------------------------
backend:
image: ${SAREX_BACKEND_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/backend:production_80d1d847}
# build:
# context: ../../generic/sarex-backend
# dockerfile: Dockerfile
container_name: sarex-backend
restart: unless-stopped
environment: &sarex-backend-env
ALLOWED_HOSTS: "*"
DJANGO_SETTINGS_MODULE: config.settings.production
JWT_PRIVATE_KEY: ${SAREX_JWT_PRIVATE_KEY:-JWT}
JWT_PUBLIC_KEY: ${SAREX_JWT_PUBLIC_KEY:-JPK}
DJANGO_POSTGRES_HOST: postgres
DJANGO_POSTGRES_DATABASE: ${SAREX_POSTGRES_DB:-sarex_db}
DJANGO_POSTGRES_USER: ${SAREX_DJANGO_DB_USER:-django}
DJANGO_POSTGRES_PASSWORD: ${SAREX_DJANGO_DB_PASSWORD:-django-secret}
# celery: брокер rabbitmq + result backend redis (pydantic env_prefix CELERY_*)
CELERY_RABBITMQ_HOST: rabbitmq
CELERY_RABBITMQ_USER: ${SAREX_RABBITMQ_USER:-rabbit}
CELERY_RABBITMQ_PASSWORD: ${SAREX_RABBITMQ_PASSWORD:-rabbit}
CELERY_RABBITMQ_VHOST: ${SAREX_RABBITMQ_VHOST:-api}
CELERY_REDIS_HOST: redis
# S3 / объектное хранилище → MinIO (сервис minio)
S3_HOST: http://minio:9000
AWS_S3_ENDPOINT_URL: http://minio:9000
S3_LOGIN: ${SAREX_MINIO_APP_USER:-sarex-app}
S3_PASSWORD: ${SAREX_MINIO_APP_PASSWORD:-sarex-app-secret}
S3_BUCKET: ${SAREX_MINIO_BUCKET:-sarex-media-storage}
S3_REGION: ${SAREX_MINIO_REGION:-us-east-1}
# Сервис measurements (MeasurementSettings, env_prefix MEASUREMENTS_)
MEASUREMENTS_HOST: ${SAREX_MEASUREMENTS_HOST:-http://measurements:8000/}
# Домен платформы. HOST/SERVER_HOST → абсолютные URL, включая media
# (CustomS3Boto3Storage.url = HOST + /media/ + key). Без этого base.py
# берёт дефолт lk.sarex.io и media-ссылки уходят на чужой хост.
SERVER_HOST: https://${PLATFORM_DOMAIN:-sarex.local}
SERVER_API_HOST: https://${PLATFORM_DOMAIN:-sarex.local}
# Media: django-storage (S3). Backend отдаёт /media/<key>, верхний nginx
# проксирует их в s3-proxy (CORS+Range). Из apps/django/base/backend-deployment.yaml.
SERVER_USE_DJANGO_STORAGE: "1"
SERVER_DJANGO_URLS: "1"
# processing (workflows-api): backend ходит в processing-api за workflow'ами.
# WORKFLOWSSETTINGS.host строит COMPARISON/DOCUMENTATION/PDM_FILES_API_URL.
# По умолчанию use=False и host=api.sarex.io — переопределяем на наш стек.
WORKFLOWS_USE: "1"
WORKFLOWS_HOST: http://processing-api:8080
WORKFLOWS_BASE_HOST: http://backend:8000
WORKFLOWS_PREFIX: /internal/v1
tty: true
ports:
- "8000:8000" # HTTP API (uWSGI http-socket), migrate выполняется в entrypoint
volumes:
# свой uwsgi.ini поверх образного: http-socket + 4 воркера (:z — SELinux/RedOS)
- ./backend/uwsgi.ini:/opt/sarex/uwsgi.ini:ro,z
# production.py с включённой классической JWT-аутентификацией
- ./backend/production.py:/opt/sarex/config/settings/production.py:ro,z
depends_on:
postgres:
condition: service_healthy
postgres-init:
condition: service_completed_successfully
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
minio-init:
condition: service_completed_successfully
celery:
image: ${SAREX_BACKEND_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/backend:production_80d1d847}
container_name: sarex-celery
restart: unless-stopped
entrypoint: celery -A config worker -l info -E -Q default -n default_worker.%h --concurrency=1 -B
environment:
<<: *sarex-backend-env
volumes:
- ./backend/production.py:/opt/sarex/config/settings/production.py:ro,z
depends_on:
postgres:
condition: service_healthy
postgres-init:
condition: service_completed_successfully
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
minio-init:
condition: service_completed_successfully
frontend:
image: ${SAREX_FRONTEND_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/frontend:contour_42138486}
container_name: sarex-frontend
restart: unless-stopped
ports:
- "8080:80" # web UI
depends_on:
- backend
# ---------------------------------------------------------------------------
# s3-proxy — отдаёт объекты MinIO/S3 браузеру с CORS и поддержкой Range (media).
# В k8s креды приходят из Vault в /vault/secrets/s3; здесь Vault нет, поэтому
# entrypoint из манифеста gracefully пропускает отсутствующий файл, а
# AWS_*-переменные подставляем напрямую (MinIO app-пользователь + бакет).
# Источник: apps/django/base/s3-proxy.yaml.
# ---------------------------------------------------------------------------
s3-proxy:
image: ${SAREX_S3_PROXY_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/s3-proxy:vault_6e586217}
container_name: s3-proxy
restart: unless-stopped
entrypoint: ["/bin/sh", "-ec"]
command:
- |
set -a
[ -f /vault/secrets/s3 ] && . /vault/secrets/s3
set +a
exec /s3-proxy
environment:
ACCESS_LOG: "true"
CORS_ALLOW_HEADERS: "Content-Type, Accept-Ranges, Content-Range, Content-Encoding"
# Замена Vault-секрета /vault/secrets/s3 прямыми значениями:
AWS_API_ENDPOINT: http://minio:9000
AWS_S3_BUCKET: ${SAREX_MINIO_BUCKET:-sarex-media-storage}
AWS_ACCESS_KEY_ID: ${SAREX_MINIO_APP_USER:-sarex-app}
AWS_SECRET_ACCESS_KEY: ${SAREX_MINIO_APP_PASSWORD:-sarex-app-secret}
# Порт наружу не публикуем — доступ только через верхний nginx (/media/),
# как ClusterIP в k8s. Внутренняя docker-сеть резолвит s3-proxy:80.
depends_on:
minio-init:
condition: service_completed_successfully
# ---------------------------------------------------------------------------
# Приложение processing (workflows-api + workflows-frontend).
# Образы приватные (cr.yandex). БД — отдельная workflow_db в общем postgres.
# Версии/env взяты из apps/processing/base/*.yaml.
# ---------------------------------------------------------------------------
processing-api:
image: ${SAREX_PROCESSING_API_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-api_prod:075fc0}
# Локальная сборка (Go, cmd/httpserver). Исходник — на dev-машине:
# build:
# context: /mnt/c/Users/user/PycharmProjects/platform/workflows-api
# dockerfile: Dockerfile
container_name: processing-api
restart: unless-stopped
entrypoint: ["/httpserver"]
command: ["migrate"] # в манифесте: /httpserver migrate (миграции + сервер на :8080)
environment:
HTTP_HOST: 0.0.0.0:8080
POSTGRES_ADDRESS: postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: ${SAREX_PROCESSING_DB:-workflow_db}
POSTGRES_USER: ${SAREX_PROCESSING_DB_USER:-processing}
POSTGRES_PASSWORD: ${SAREX_PROCESSING_DB_PASSWORD:-processing-secret}
POSTGRES_POOL_SIZE: "3"
POSTGRES_SSL_USE: "0"
ENABLE_SQL_QUERY: "0"
# JWT public key (общий с backend) — единственная интеграция, ей
# workflows-api проверяет Bearer-токен на /api. Больше код ничего не читает
# (ни S3, ни DJANGO_HOST — они в манифесте были мёртвыми).
PUBLIC_KEY: ${SAREX_JWT_PUBLIC_KEY:-JPK}
# Порт наружу не публикуем — доступ через верхний nginx (/workflows/api/).
depends_on:
postgres:
condition: service_healthy
postgres-init:
condition: service_completed_successfully
processing-frontend:
image: ${SAREX_PROCESSING_FRONTEND_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-frontend:ugok2_85f6ce2c}
# Локальная сборка (node, npm run build-module). Исходник — на dev-машине.
# Нужны build-args BUILD_ENV=contour и NPM_NEXUS_TOKEN (приватный npm).
# build:
# context: /mnt/c/Users/user/WebstormProjects/platform/workflows-frontend
# dockerfile: Dockerfile
# args:
# BUILD_ENV: contour
# NPM_NEXUS_TOKEN: ${NPM_NEXUS_TOKEN:-}
container_name: processing-frontend
restart: unless-stopped
# Порт наружу не публикуем — доступ через верхний nginx (/workflows/).
depends_on:
- processing-api
# ---------------------------------------------------------------------------
# workflows-engine — оркестратор processing: читает workflow_db, создаёт Job'ы
# в k3s (K8s-исполнитель; AMQP выкл). В кластер ходит out-of-cluster через
# kubeconfig (KUBE_CONFIG+KUBE_ADDR). Job-поды резолвят postgres/minio через
# DNS-мост namespace processing. Источник: apps/processing/base/engine*.yaml.
# ---------------------------------------------------------------------------
engine:
image: ${SAREX_WORKFLOWS_ENGINE_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/workflows-endigne_prod:075fc0}
container_name: engine
restart: unless-stopped
entrypoint: ["/engine"]
environment:
APP_NAME: workflows-engine
LOG_LEVEL: info
ENVIRONMENT: contour
# исполнители: только Kubernetes
ENABLE_KUBERNETES_EXECUTOR: "1"
ENABLE_AMQP_EXECUTOR: "0"
COUNT_RUNNING_WORKERS: "1"
COUNT_CANCELING_WORKERS: "1"
COUNT_HANDLE_JOB_WORKERS: "1"
WORKFLOW_PRIORITY: low
MAX_WORKFLOWS_LIMIT: "5"
# доступ в k3s (out-of-cluster): kubeconfig + перекрытый адрес apiserver
KUBE_CONFIG: /kube/config
KUBE_CONTEXT: default
KUBE_ADDR: https://k3s-server:6443
JOBS_NAMESPACE: processing
# свой доступ к workflow_db по compose-DNS
POSTGRES_ADDRESS: postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: ${SAREX_PROCESSING_DB:-workflow_db}
POSTGRES_USER: ${SAREX_PROCESSING_DB_USER:-processing}
POSTGRES_PASSWORD: ${SAREX_PROCESSING_DB_PASSWORD:-processing-secret}
POSTGRES_POOL_SIZE: "20"
POSTGRES_SSL_USE: "0"
# хранилища: только S3, напрямую в minio (yc-s3 SA JSON смонтирован)
ENABLE_S3_STORAGE: "1"
S3_SERVICE_ACCOUNT: /etc/sarex/yc-s3/yc-s3-service-account.json
# дефолты планирования Job-подов
DEFAULT_IMAGE_PULL_POLICY: IfNotPresent
DEFAULT_CPU_REQUESTS: 100m
DEFAULT_MEMORY_REQUESTS: 64Mi
volumes:
- ./k3s/kubeconfig.yaml:/kube/config:ro,z
- ./engine/yc-s3-service-account.json:/etc/sarex/yc-s3/yc-s3-service-account.json:ro,z
depends_on:
postgres:
condition: service_healthy
postgres-init:
condition: service_completed_successfully
k3s-server:
condition: service_healthy
processing-k8s-init:
condition: service_completed_successfully
# ---------------------------------------------------------------------------
# measurements — FastAPI, считает измерения по растрам из S3/MinIO (GDAL vsis3).
# БД/воркера нет. Обязателен S3_JSON_SETTINGS. Источник: apps/measurements/base.
# ---------------------------------------------------------------------------
measurements:
image: ${SAREX_MEASUREMENTS_IMAGE:-cr.yandex/crp3ccidau046kdj8g9q/measurements:production_a5bb3e84}
container_name: measurements
restart: unless-stopped
entrypoint: ["/opt/entrypoint.sh"]
environment:
# host http:// → GDAL переключается на AWS_HTTPS=NO (см. CONFIGURATION.md)
S3_JSON_SETTINGS: '{"host":"http://minio:9000","login":"${SAREX_MINIO_APP_USER:-sarex-app}","password":"${SAREX_MINIO_APP_PASSWORD:-sarex-app-secret}","verify":false,"buckets":["${SAREX_MINIO_BUCKET:-sarex-media-storage}"]}'
LOG_LEVEL: INFO
TRACING_USE: "false"
AUTH: "0"
DJANGO_HOST: http://backend:8000
# порт наружу не публикуем — доступ только из backend по http://measurements:8000/
depends_on:
minio-init:
condition: service_completed_successfully
# Верхний reverse-proxy: 3 домена → платформа / rabbitmq (admin) / minio
nginx:
image: ${NGINX_IMAGE:-nginx:1.27-alpine}
container_name: sarex-nginx
restart: unless-stopped
ports:
- "80:80" # http
- "443:443" # https (самоподписанный сертификат)
environment:
# подставляются envsubst'ом в шаблон при старте контейнера
PLATFORM_DOMAIN: ${PLATFORM_DOMAIN:-sarex.local}
ADMIN_DOMAIN: ${ADMIN_DOMAIN:-admin.sarex.local}
MINIO_DOMAIN: ${MINIO_DOMAIN:-minio.sarex.local}
volumes:
- ./nginx/templates:/etc/nginx/templates:ro,z # :z — SELinux label для RedOS
- ./nginx/certs:/etc/nginx/certs:ro,z # TLS-сертификаты
depends_on:
# - backend # TODO: закомментированы в шаблоне nginx, пока в работе
# - frontend
- minio
- rabbitmq
- s3-proxy
- processing-api
- processing-frontend
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"]
interval: 10s
timeout: 5s
retries: 6
start_period: 10s
volumes:
k3s-server-data:
k3s-worker-1-data:
k3s-worker-2-data:
k3s-worker-3-data:
gitea-data:
sarex-vault-data: # file-хранилище Vault
sarex-vault-init: # unseal-ключ и root-токен (только для vault-init)
sarex-postgres-data:
sarex-redis-data:
sarex-rabbitmq-data:
sarex-minio-data:
# Дефолтной сети задаём фиксированный subnet, чтобы закрепить IP за postgres/minio.
# На эти адреса ссылаются k8s-Endpoints DNS-моста (namespace processing в k3s).
networks:
default:
ipam:
config:
- subnet: 172.28.0.0/16