iac/docker-compose.yaml
emelinda 75d1fba14a Compose: убран прикладной стек — он давно переехал в k3s
В docker-compose.yaml описывались 25 сервисов, а на хосте существовали
контейнеры только для 9. Остальные 16 — django, celery, frontend, s3-proxy,
processing-api/frontend, engine, measurements, nginx и их зависимости
(postgres, redis, rabbitmq, minio с init-контейнерами) — развёрнуты в k3s и
описаны в apps/ и infrastructure/. Дубликат не просто лежал мёртвым грузом,
а расходился с кластером и вводил в заблуждение.

Что это был именно мёртвый груз, а не «выключенный на время» стек:

  * контейнеров не существовало вовсе — не Exited, а не создавались;
  * именованных томов sarex-postgres-data, sarex-redis-data,
    sarex-rabbitmq-data, sarex-minio-data в docker volume ls нет;
  * стек был взаимно несовместим: nginx публиковал 80/443, те же порты
    публикует k3s-server (внутри его контейнера на них сидит istio
    ingressgateway), и docker compose up nginx упал бы на bind;
  * подъём был выключен по умолчанию (sarex_compose_up: false).

Заодно убрано всё, что обслуживало только удалённые сервисы:

  * sarex_services, sarex_compose_up, задача up и poe-таски stack/up;
  * оба handler'а (пересоздание nginx/backend/celery) — файл handlers
    остался пустым и удалён;
  * генерация самоподписанного сертификата и cert_dir: TLS выписывает
    cert-manager внутри кластера, верхнего nginx больше нет;
  * каталоги nginx/, backend/, engine/ и задачи их копирования;
  * k3s/manifests/processing — DNS-мост на IP compose-postgres/minio,
    которых больше не существует (в кластере он и не был создан).

poe install теперь идёт через platform, а не через удалённый stack — это
заодно чинит старую дыру, из-за которой install не поднимал подложку.

Проверено: docker compose config отдаёт ровно 9 сервисов, совпадающих с
docker ps на aero-01; ansible-lint новых замечаний не даёт.
2026-08-10 12:55:04 +03:00

416 lines
22 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.

# GitOps-ПОДЛОЖКА КОНТУРА, И ТОЛЬКО ОНА.
#
# Здесь описано ровно то, что не может жить внутри кластера, потому что кластер
# на этом и стоит: сами ноды k3s, git-репозиторий-источник (gitea), хранилище
# секретов (vault) и одноразовые установщики, доводящие всё это до состояния,
# в котором за дело берётся Flux.
#
# ПРИКЛАДНОГО СТЕКА ЗДЕСЬ НЕТ И БЫТЬ НЕ ДОЛЖНО. Django, celery, frontend,
# processing, measurements, bim, а также postgres, redis, rabbitmq и minio
# развёрнуты в k3s и описаны декларативно в apps/ и infrastructure/; источник
# правды для них — репозиторий в gitea, а не этот файл. Раньше они дублировались
# и здесь; дубликат удалён, потому что расходился с кластером и вводил в
# заблуждение. Возвращать их сюда не надо: nginx конфликтовал бы с k3s-server за
# порты 80/443 (их занимает istio ingressgateway внутри контейнера ноды), а
# вторая копия СУБД тихо разъехалась бы с той, где лежат данные.
#
# Поднимает всё это ansible-роль sarex_stack тремя волнами — см. aero/roles/
# sarex_stack/tasks/platform.yml, порядок там значим.
# Общая конфигурация воркер-нод k3s — см. сервисы k3s-worker-1..2.
# У каждой ноды СВОЙ том состояния и СВОЙ каталог 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
# Встроенный в k3s local-path-provisioner отключён: его заменяет чарт
# local-path-provisioner из репозитория (clusters/aero/controllers).
# Два провижинера с одним именем StorageClass дрались бы за одни и те же
# PVC. Каталог данных при этом НЕ меняется — чарту задан тот же путь
# /var/lib/rancher/k3s/storage, который смонтирован с хоста ниже.
- --disable=local-storage
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:
# Под istio ingressgateway занимает hostPort 80/443 ВНУТРИ этого
# контейнера и через nodeAffinity живёт только на control-plane, то есть
# именно здесь. Публикация этих портов наружу — единственное, что отделяет
# сервисы контура от внешнего мира; маршрутизация дальше по домену задаётся
# в infrastructure/istio-config/aero (Gateway + VirtualService).
- "80:80" # ingress http
- "443:443" # ingress https
# kube-apiserver наружу НЕ публикуем: доступ к нему — только изнутри
# compose-сети, по имени k3s-server:6443 (так к нему ходят flux-bootstrap
# и одноразовые init-контейнеры).
# Если понадобится внешний kubectl — раскомментируй и ОБЯЗАТЕЛЬНО
# ограничь фаерволом до доверенных адресов.
# - "6443:6443"
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
# ---------------------------------------------------------------------------
# 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
# Статический IP: на него ссылается k8s-Endpoints `vault` (DNS-мост в k3s,
# namespace vault). Через этот мост vault-agent-injector и внедрённые в поды
# агенты ходят к Vault, который живёт здесь, в compose, а не в кластере.
networks:
default:
ipv4_address: 172.28.0.13
# Инициализация и авто-распечатывание 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.
#
# --interval=2m задаёт периодичность проверок у GitRepository и корневой
# Kustomization в clusters/aero/flux-system/gotk-sync.yaml. Задаётся
# именно флагом, потому что этот файл генерирует сам bootstrap и правка
# руками затирается при следующем прогоне (в нём так и написано:
# "DO NOT EDIT"). Дефолтные 10 минут растягивали цикл «правка → проверка»
# до получаса.
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 \
--interval=2m
volumes:
k3s-server-data:
k3s-worker-1-data:
k3s-worker-2-data:
gitea-data:
sarex-vault-data: # file-хранилище Vault
sarex-vault-init: # unseal-ключ и root-токен (только для vault-init)
# Дефолтной сети задаём фиксированный subnet, чтобы закрепить IP за gitea и vault.
# На эти адреса ссылаются k8s-Endpoints DNS-мостов внутри k3s (namespace
# flux-system и vault): у кластера своя DNS, и имена compose-сети в ней не
# резолвятся, а поды дотягиваются до 172.28.0.0/16 через SNAT на ноде.
# Данные PVC (postgres, minio, rabbitmq) живут в k3s и томов здесь не имеют.
networks:
default:
ipam:
config:
- subnet: 172.28.0.0/16