# 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