feat(ansible): add GitOps foundation (k3s, gitea, vault, and FluxCD) with multi-wave deployment support
This commit is contained in:
parent
e39404cb4c
commit
8a8cd66f59
18
.env.example
18
.env.example
@ -1,4 +1,4 @@
|
||||
K3S_TOKEN=gVadzB0OtWGDAM9rmlZvHiwfITsP8bxo
|
||||
K3S_TOKEN=changeme
|
||||
|
||||
# Внешний адрес (IP или домен), по которому к kube-apiserver обращаются извне.
|
||||
# Попадёт в TLS-сертификат (--tls-san). Без него внешний kubectl упадёт на x509.
|
||||
@ -48,9 +48,25 @@ PLATFORM_DOMAIN=sarex.local # основной домен платфо
|
||||
ADMIN_DOMAIN=admin.sarex.local # RabbitMQ management UI
|
||||
MINIO_DOMAIN=minio.sarex.local # MinIO: S3 API (/) + консоль (/console/)
|
||||
|
||||
# --- GitOps-подложка: gitea (источник правды) + Vault + FluxCD в k3s ---
|
||||
# Админ gitea заводится контейнером gitea-init; этими же кредами flux-bootstrap
|
||||
# авторизуется при push'е манифестов. Пароль генерирует роль sarex_stack.
|
||||
GITEA_ADMIN_USER=sarex
|
||||
GITEA_ADMIN_PASSWORD=changeme
|
||||
GITEA_ADMIN_EMAIL=sarex@sarex.local
|
||||
# Организация и репозиторий, которые gitea-init создаёт, а Flux реконсилирует.
|
||||
GITEA_ORG=infra
|
||||
GITEA_REPO=iac
|
||||
# Ветка и путь внутри репозитория, за которыми следит Flux.
|
||||
AERO_GIT_BRANCH=master
|
||||
AERO_FLUX_PATH=clusters/aero
|
||||
|
||||
# --- Docker-образы инфраструктуры (тег/реестр можно переопределить) ---
|
||||
K3S_IMAGE=rancher/k3s:v1.36.2-k3s1
|
||||
GITEA_IMAGE=gitea/gitea:1.22
|
||||
VAULT_IMAGE=hashicorp/vault:1.18
|
||||
# Версия CLI должна совпадать с версией контроллеров Flux в остальных кластерах.
|
||||
FLUX_CLI_IMAGE=ghcr.io/fluxcd/flux-cli:v2.8.5
|
||||
POSTGRES_IMAGE=postgres:12-alpine
|
||||
NGINX_IMAGE=nginx:1.27-alpine
|
||||
SAREX_REDIS_IMAGE=cr.yandex/crp3ccidau046kdj8g9q/redis:7.0.10
|
||||
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@ -2,7 +2,22 @@
|
||||
.claude
|
||||
CLAUDE.md
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
tmp
|
||||
.tmp/
|
||||
snapshots/*
|
||||
|
||||
# --- Рантайм-артефакты контура aero (создаются docker-compose на хосте) ---
|
||||
gitea-data/ # данные gitea: репозитории, sqlite
|
||||
k3s-storage/ # PVC-данные local-path-provisioner
|
||||
k3s/kubeconfig.yaml # kubeconfig, который пишет k3s-server
|
||||
nginx/certs/ # самоподписанные TLS-сертификаты
|
||||
|
||||
# --- Секреты и ключи: не коммитить никогда ---
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
kubeconfig*
|
||||
*service-account*.json
|
||||
|
||||
@ -63,10 +63,13 @@ uv run poe install
|
||||
| `uv run poe ping` | проверка SSH + sudo |
|
||||
| `uv run poe provision` | базовые пакеты + docker/containerd + запуск службы |
|
||||
| `uv run poe deploy` | деплой конфигурации: файлы, `.env`, сертификаты, `docker login` |
|
||||
| `uv run poe platform` | деплой + поднять GitOps-подложку (k3s + gitea + vault + Flux) |
|
||||
| `uv run poe stack` | деплой + поднять стек (`docker compose up -d`) |
|
||||
| `uv run poe gen-env` | только сгенерировать пароли и `.env` |
|
||||
| `uv run poe login` | только `docker login` в реестр по ключу |
|
||||
| `uv run poe superuser` | сгенерировать логин+пароль и создать Django-суперпользователя |
|
||||
| `uv run poe flux-status` | состояние FluxCD в k3s |
|
||||
| `uv run poe vault-status` | состояние Vault (инициализирован / распечатан) |
|
||||
| `uv run poe check` / `deploy-check` | dry-run (`--check --diff`) |
|
||||
|
||||
Поднимаемые сервисы задаются в `sarex_services` (роль `sarex_stack`) —
|
||||
@ -77,6 +80,100 @@ uv run poe install
|
||||
> (через `/mnt/c`) права `0777`, и ansible игнорирует `ansible.cfg` в
|
||||
> world-writable каталоге, поэтому путь к конфигу передаётся явно.
|
||||
|
||||
### GitOps-подложка контура (k3s + gitea + vault + FluxCD)
|
||||
|
||||
Целевая схема контура — не «всё в compose», а **compose как подложка, k3s как
|
||||
среда выполнения**. Источником правды становится gitea внутри контура, а
|
||||
раскаткой занимается FluxCD.
|
||||
|
||||
```
|
||||
docker compose k3s (4 ноды в контейнерах)
|
||||
├── k3s-server ──► нода ┐
|
||||
├── k3s-worker-1..3 ──► ноды ├─► flux-system (controllers)
|
||||
├── gitea (172.28.0.12) ◄────── ┘ и всё, что опишем в clusters/aero
|
||||
│ └── infra/iac.git
|
||||
├── vault (+init)
|
||||
├── flux-k8s-init ──► namespace flux-system + Service-мост до gitea
|
||||
└── flux-bootstrap ──► ставит Flux в k3s и привязывает к gitea
|
||||
```
|
||||
|
||||
**Мост до gitea.** Контроллеры Flux работают внутри k3s, где резолвит CoreDNS,
|
||||
а имён compose-сети там нет. Поэтому `flux-k8s-init` заводит в namespace
|
||||
`flux-system` Service без селектора + Endpoints на статический IP gitea — тем же
|
||||
приёмом, что `bridge-postgres`/`bridge-minio` для processing. Источник в
|
||||
`GitRepository` указан именем `gitea.flux-system.svc.cluster.local`, а адрес
|
||||
задан ровно в одном месте — переменной `GITEA_BRIDGE_IP` (она же `ipv4_address`
|
||||
сервиса `gitea`). Контейнер `flux-bootstrap` резолвит то же имя через
|
||||
`extra_hosts` — иначе flux CLI не смог бы запушить манифесты.
|
||||
|
||||
**Имена нод закреплены** через `hostname:` в compose. Это не косметика: k3s
|
||||
берёт имя ноды из hostname, иначе им становится ID контейнера. Local-path
|
||||
привязывает PV к ноде через `nodeAffinity` по имени, и после пересоздания
|
||||
контейнера тома «повисли» бы.
|
||||
|
||||
### Хранилище PVC
|
||||
|
||||
`StorageClass local-path` (встроенный в k3s, он же default). Дефолтный путь
|
||||
провижинера `/var/lib/rancher/k3s/storage` перекрыт bind-mount'ом на хост —
|
||||
поэтому настраивать ConfigMap `local-path-config` не нужно:
|
||||
|
||||
```
|
||||
{{ deploy_dir }}/k3s-storage/
|
||||
├── server/ ← PVC, севшие на k3s-server
|
||||
├── worker-1/
|
||||
├── worker-2/
|
||||
└── worker-3/
|
||||
```
|
||||
|
||||
Файлы лежат на хосте обычными каталогами: переживают пересоздание контейнера
|
||||
k3s и `docker compose down -v`, бэкапятся штатными средствами.
|
||||
|
||||
> **Следствие для stateful-сервисов.** Хранилище **node-local**: PVC привязан к
|
||||
> той ноде, где под запустился первым. При переезде postgres/redis/rabbitmq/minio
|
||||
> в k3s их нужно явно прибивать к конкретной ноде (`nodeSelector`), иначе данные
|
||||
> размажутся по четырём каталогам, а потеря ноды сделает том недоступным.
|
||||
> Бэкап должен покрывать все каталоги `k3s-storage/*`.
|
||||
|
||||
```bash
|
||||
uv run poe platform # поднять подложку
|
||||
uv run poe flux-status # убедиться, что Flux реконсилирует
|
||||
```
|
||||
|
||||
Что происходит по шагам:
|
||||
|
||||
1. **Волна 1** — стартуют `k3s-server`, три воркера и `gitea`. Роль ждёт, пока
|
||||
k3s запишет `./k3s/kubeconfig.yaml` (и что файл непустой) — иначе
|
||||
`flux-bootstrap` смонтировал бы каталог вместо файла.
|
||||
2. **`gitea-init`** — одноразовый: заводит администратора (`gitea admin user
|
||||
create`), организацию и пустой репозиторий через API. Идемпотентен.
|
||||
3. **`flux-k8s-init`** — одноразовый: namespace `flux-system` и Service-мост до
|
||||
gitea (см. выше). Обязан отработать до bootstrap.
|
||||
4. **`vault` + `vault-init`** — Vault на file-хранилище. `vault-init` живёт
|
||||
постоянно: инициализирует Vault одним ключом, распечатывает его и включает
|
||||
`kv-v2` на пути `secrets/` (тот же путь, что в k8s-контурах). После ребута
|
||||
хоста Vault поднимается запечатанным — цикл распечатывает его сам.
|
||||
5. **`flux-bootstrap`** — одноразовый: `flux bootstrap git` ставит контроллеры в
|
||||
k3s и пушит их манифесты в gitea в `clusters/aero/flux-system`.
|
||||
|
||||
> **FluxCD не работает в docker-compose** — это контроллеры Kubernetes. В compose
|
||||
> живёт только установщик; после его успеха Flux работает внутри k3s.
|
||||
|
||||
Секреты подложки (`GITEA_ADMIN_PASSWORD`, `K3S_TOKEN`) генерируются так же, как
|
||||
остальные — в `aero/.secrets/<host>/`. Unseal-ключ и root-токен Vault лежат на
|
||||
отдельном docker-томе `sarex-vault-init` с правами `600` и **в git не попадают**.
|
||||
|
||||
#### Границы текущего этапа
|
||||
|
||||
Сделана только подложка. Осознанно **не** сделано:
|
||||
|
||||
- Репозиторий в gitea содержит лишь `clusters/aero/flux-system` — то, что запушил
|
||||
bootstrap. Наполнение (`apps/`, `infrastructure/`) и перенос прикладных сервисов
|
||||
из compose в k3s — следующий этап.
|
||||
- Vault поднят и распечатан, но **kubernetes auth не настроен**: для него нужен
|
||||
Vault Agent Injector внутри k3s, а он приедет уже через Flux. Пока в Vault
|
||||
можно только класть секреты, поды их ещё не читают.
|
||||
- Прикладной стек (`sarex_services`) не тронут и продолжает работать в compose.
|
||||
|
||||
### Развёртывание sarex-стека (роль `sarex_stack`)
|
||||
|
||||
Копирует `docker-compose.yaml`, `nginx/templates` и `backend/uwsgi.ini` в
|
||||
|
||||
@ -36,6 +36,10 @@ deploy = "ansible-playbook deploy.yml"
|
||||
deploy-check = "ansible-playbook deploy.yml --check --diff"
|
||||
# Деплой + поднять стек приложения (docker compose up -d)
|
||||
stack = "ansible-playbook deploy.yml -e sarex_compose_up=true"
|
||||
# Деплой + поднять GitOps-подложку: k3s + gitea + vault + bootstrap FluxCD.
|
||||
# Без --tags намеренно: подложке нужны .env, docker-compose.yaml и vault/config,
|
||||
# которые кладут нетегированные задачи роли (при --tags они были бы пропущены).
|
||||
platform = "ansible-playbook deploy.yml -e aero_platform_up=true"
|
||||
# Только генерация паролей и рендер .env на хосте (без копирования/сертификатов)
|
||||
gen-env = "ansible-playbook deploy.yml --tags env"
|
||||
# docker login в приватный реестр по ключу json_key
|
||||
@ -53,6 +57,14 @@ up = "ansible-playbook deploy.yml -e sarex_compose_up=true --tags up"
|
||||
cmd = "ansible docker_hosts -m ansible.builtin.shell -a 'docker compose ps chdir=/root/sarex'"
|
||||
help = "Статус стека на хосте (docker compose ps)"
|
||||
|
||||
[tool.poe.tasks.flux-status]
|
||||
cmd = "ansible docker_hosts -m ansible.builtin.shell -a 'docker compose exec -T k3s-server kubectl -n flux-system get kustomizations,gitrepositories,pods chdir=/root/sarex'"
|
||||
help = "Состояние FluxCD в k3s (kustomizations, источники, поды)"
|
||||
|
||||
[tool.poe.tasks.vault-status]
|
||||
cmd = "ansible docker_hosts -m ansible.builtin.shell -a 'docker compose exec -T vault-init vault status chdir=/root/sarex'"
|
||||
help = "Состояние Vault (инициализирован / распечатан)"
|
||||
|
||||
[tool.poe.tasks.pull]
|
||||
cmd = "ansible docker_hosts -m ansible.builtin.shell -a 'docker compose pull ${service} chdir=/root/sarex'"
|
||||
help = "Стянуть образы: uv run poe pull [service]"
|
||||
|
||||
@ -17,6 +17,7 @@ cert_dir: "{{ deploy_dir }}/nginx/certs"
|
||||
# Переменные-пароли: им генерируются стойкие значения взамен слабых дефолтов.
|
||||
sarex_secret_vars:
|
||||
- K3S_TOKEN
|
||||
- GITEA_ADMIN_PASSWORD
|
||||
- SAREX_POSTGRES_PASSWORD
|
||||
- SAREX_DJANGO_DB_PASSWORD
|
||||
- SAREX_PROCESSING_DB_PASSWORD
|
||||
@ -46,6 +47,51 @@ registry_key_src: /mnt/c/Users/user/.ssh/local/authorized_key.json
|
||||
sarex_compose_up: false
|
||||
compose_cmd: "docker compose"
|
||||
|
||||
# --- GitOps-подложка контура (k3s + gitea + vault + flux) -------------------
|
||||
# Параметры gitea и Flux. Попадают в .env (их читает docker-compose) — менять
|
||||
# нужно здесь, а не в .env.example, иначе перезапишется при следующем деплое.
|
||||
gitea_admin_user: sarex
|
||||
gitea_admin_email: "{{ gitea_admin_user }}@{{ platform_domain }}"
|
||||
gitea_org: infra
|
||||
gitea_repo: iac
|
||||
# Ветка и путь, за которыми следит Flux внутри репозитория в gitea.
|
||||
aero_git_branch: master
|
||||
aero_flux_path: clusters/aero
|
||||
|
||||
# Поднимается отдельно от прикладного стека: образы публичные, docker login в
|
||||
# cr.yandex не нужен. По умолчанию выключено, включает `uv run poe platform`.
|
||||
aero_platform_up: false
|
||||
|
||||
# Волна 1: долгоживущие сервисы, которым нужно успеть стать healthy.
|
||||
# k3s-server при первом старте создаёт ./k3s/kubeconfig.yaml — его ждёт волна 2.
|
||||
aero_platform_services_wave1:
|
||||
- k3s-server
|
||||
- k3s-worker-1
|
||||
- k3s-worker-2
|
||||
- k3s-worker-3
|
||||
- gitea
|
||||
|
||||
# Волна 2: провижининг и bootstrap. gitea-init, flux-k8s-init и flux-bootstrap
|
||||
# одноразовые (restart: "no"), vault-init живёт постоянно и распечатывает Vault
|
||||
# после ребута хоста.
|
||||
aero_platform_services_wave2:
|
||||
- gitea-init
|
||||
- flux-k8s-init
|
||||
- vault
|
||||
- vault-init
|
||||
- flux-bootstrap
|
||||
|
||||
# Ноды k3s. Каждой нужен свой каталог PVC-хранилища на хосте: провижинер
|
||||
# local-path node-local, и PV навсегда привязывается к ноде через nodeAffinity.
|
||||
aero_k3s_nodes:
|
||||
- server
|
||||
- worker-1
|
||||
- worker-2
|
||||
- worker-3
|
||||
|
||||
# Сколько ждать появления kubeconfig от k3s-server, секунд.
|
||||
aero_kubeconfig_timeout: 180
|
||||
|
||||
# Сервисы приложения, поднимаемые при sarex_compose_up (depends_on тянет
|
||||
# зависимости и порядок). k3s/gitea намеренно не поднимаем.
|
||||
sarex_services:
|
||||
|
||||
@ -50,6 +50,22 @@
|
||||
dest: "{{ deploy_dir }}/engine/"
|
||||
mode: "0644"
|
||||
|
||||
# vault монтирует ./vault/config как :ro — без конфига сервер не стартует.
|
||||
- name: Скопировать конфигурацию Vault
|
||||
ansible.builtin.copy:
|
||||
src: "{{ deploy_src_root }}/vault/config/"
|
||||
dest: "{{ deploy_dir }}/vault/config/"
|
||||
mode: "0644"
|
||||
|
||||
# Каталоги PVC-данных k3s — по одному на ноду. Создаём заранее: иначе docker
|
||||
# создаст их от root с umask демона, а local-path пишет туда от имени подов.
|
||||
- name: Создать каталоги PVC-хранилища k3s (по ноде)
|
||||
ansible.builtin.file:
|
||||
path: "{{ deploy_dir }}/k3s-storage/{{ item }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
loop: "{{ aero_k3s_nodes }}"
|
||||
|
||||
# --- Секреты: генерация с персистом на control-node (идемпотентно) --------
|
||||
- name: Сгенерировать/загрузить пароли (persist в secrets_store, gitignored)
|
||||
ansible.builtin.set_fact:
|
||||
@ -88,6 +104,12 @@
|
||||
'ADMIN_DOMAIN': admin_domain,
|
||||
'MINIO_DOMAIN': minio_domain,
|
||||
'SAREX_MINIO_CONSOLE_URL': 'https://' ~ minio_domain,
|
||||
'GITEA_ADMIN_USER': gitea_admin_user,
|
||||
'GITEA_ADMIN_EMAIL': gitea_admin_email,
|
||||
'GITEA_ORG': gitea_org,
|
||||
'GITEA_REPO': gitea_repo,
|
||||
'AERO_GIT_BRANCH': aero_git_branch,
|
||||
'AERO_FLUX_PATH': aero_flux_path,
|
||||
'SAREX_JWT_PRIVATE_KEY': "'" ~ (lookup('file', jwt_private_key_path) | replace('\n', '\\n')) ~ "'",
|
||||
'SAREX_JWT_PUBLIC_KEY': "'" ~ (lookup('file', jwt_public_key_path) | replace('\n', '\\n')) ~ "'",
|
||||
}) }}
|
||||
@ -157,6 +179,14 @@
|
||||
state: absent
|
||||
tags: [login]
|
||||
|
||||
# --- GitOps-подложка: k3s + gitea + vault + Flux --------------------------
|
||||
# Отдельный тег и отдельный флаг: подложка поднимается публичными образами и
|
||||
# не зависит от docker login, поэтому её можно накатывать независимо от стека.
|
||||
- name: Поднять GitOps-подложку контура
|
||||
ansible.builtin.include_tasks: platform.yml
|
||||
when: aero_platform_up | bool
|
||||
tags: [platform]
|
||||
|
||||
# --- Поднять сервисы приложения ------------------------------------------
|
||||
- name: Поднять сервисы приложения (docker compose up -d)
|
||||
ansible.builtin.command:
|
||||
|
||||
83
aero/roles/sarex_stack/tasks/platform.yml
Normal file
83
aero/roles/sarex_stack/tasks/platform.yml
Normal file
@ -0,0 +1,83 @@
|
||||
---
|
||||
# Подъём GitOps-подложки контура: k3s + gitea + vault + bootstrap FluxCD.
|
||||
# Отделено от прикладного стека (sarex_services) намеренно: здесь только
|
||||
# публичные образы, docker login в приватный реестр не требуется.
|
||||
#
|
||||
# Порядок важен и поэтому разбит на две волны: flux-bootstrap монтирует
|
||||
# ./k3s/kubeconfig.yaml, который k3s-server создаёт только при первом старте.
|
||||
# Если поднять всё одной командой, docker может создать на месте отсутствующего
|
||||
# файла директорию, и bootstrap упадёт с невнятной ошибкой.
|
||||
|
||||
- name: Волна 1 — поднять k3s-server и gitea
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ compose_cmd }} up -d {{ aero_platform_services_wave1 | join(' ') }}"
|
||||
chdir: "{{ deploy_dir }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Дождаться kubeconfig от k3s-server
|
||||
ansible.builtin.wait_for:
|
||||
path: "{{ deploy_dir }}/k3s/kubeconfig.yaml"
|
||||
state: present
|
||||
timeout: "{{ aero_kubeconfig_timeout }}"
|
||||
|
||||
# Пустой файл означает, что k3s-server ещё дописывает kubeconfig. Без этой
|
||||
# проверки flux-bootstrap может стартовать с нулевым конфигом.
|
||||
- name: Убедиться, что kubeconfig не пустой
|
||||
ansible.builtin.wait_for:
|
||||
path: "{{ deploy_dir }}/k3s/kubeconfig.yaml"
|
||||
search_regex: "clusters:"
|
||||
timeout: 60
|
||||
|
||||
- name: Волна 2 — gitea-init, vault(+init) и bootstrap Flux
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ compose_cmd }} up -d {{ aero_platform_services_wave2 | join(' ') }}"
|
||||
chdir: "{{ deploy_dir }}"
|
||||
changed_when: true
|
||||
|
||||
# flux-bootstrap одноразовый: успех = код 0. Забираем его явно, иначе ошибка
|
||||
# bootstrap'а останется незамеченной — `compose up -d` о ней не сообщает.
|
||||
#
|
||||
# `docker compose wait` здесь не годится: для уже завершившегося контейнера он
|
||||
# возвращает rc=1 и "no containers for project". Так как `up -d` не дожидается
|
||||
# одноразового сервиса, состояние гоночное — поэтому опрашиваем `ps -a`,
|
||||
# который корректно отвечает и для running, и для exited.
|
||||
#
|
||||
# Формат обёрнут в {% raw %}: это Go-шаблон docker'а, и без raw Jinja попыталась
|
||||
# бы раскрыть {{.State.Status}} как свою переменную. Аргументы передаём списком —
|
||||
# у `command` нет шелла, и строка со скобками разбилась бы по пробелу.
|
||||
- name: Дождаться завершения flux-bootstrap
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ compose_cmd.split()[0] }}"
|
||||
- inspect
|
||||
- "-f"
|
||||
- "{% raw %}{{.State.Status}} {{.State.ExitCode}}{% endraw %}"
|
||||
- flux-bootstrap
|
||||
register: aero_flux_bootstrap
|
||||
changed_when: false
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: aero_flux_bootstrap.stdout | trim is not match('^running')
|
||||
|
||||
- name: Показать логи flux-bootstrap при ошибке
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ compose_cmd }} logs --no-color --tail 50 flux-bootstrap"
|
||||
chdir: "{{ deploy_dir }}"
|
||||
register: aero_flux_logs
|
||||
changed_when: false
|
||||
when: aero_flux_bootstrap.stdout.split() | last != "0"
|
||||
|
||||
- name: Прервать деплой, если bootstrap не удался
|
||||
ansible.builtin.fail:
|
||||
msg: |
|
||||
flux bootstrap завершился с кодом {{ aero_flux_bootstrap.stdout.split() | last }}.
|
||||
Логи контейнера:
|
||||
{{ aero_flux_logs.stdout | default('(недоступны)') }}
|
||||
when: aero_flux_bootstrap.stdout.split() | last != "0"
|
||||
|
||||
- name: Итог
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "gitea: http://{{ ansible_host }}:3000 (пользователь {{ gitea_admin_user }}, пароль в aero/.secrets/{{ inventory_hostname }}/GITEA_ADMIN_PASSWORD)"
|
||||
- "Flux реконсилирует {{ gitea_org }}/{{ gitea_repo }} → {{ aero_flux_path }}"
|
||||
- "Проверка: docker compose exec k3s-server kubectl -n flux-system get kustomizations"
|
||||
@ -1,7 +1,33 @@
|
||||
# Общая конфигурация воркер-нод 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}
|
||||
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
|
||||
@ -22,6 +48,15 @@ services:
|
||||
volumes:
|
||||
- k3s-server-data:/var/lib/rancher/k3s # etcd/state, образы, манифесты — только named volume
|
||||
- ./k3s:/output:z # отсюда забираем контекст k8s (:z — SELinux label для RedOS)
|
||||
# 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
|
||||
@ -37,30 +72,32 @@ services:
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
|
||||
k3s-worker:
|
||||
image: ${K3S_IMAGE:-rancher/k3s:v1.36.2-k3s1}
|
||||
container_name: k3s-worker
|
||||
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}
|
||||
# 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-data:/var/lib/rancher/k3s
|
||||
tmpfs:
|
||||
- /run
|
||||
- /var/run
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
- k3s-worker-1-data:/var/lib/rancher/k3s
|
||||
- ./k3s-storage/worker-1:/var/lib/rancher/k3s/storage: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-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: namespace processing + DNS-мост postgres/minio.
|
||||
# Образ k3s содержит kubectl; server/tls перекрываем на k3s-server:6443
|
||||
@ -82,6 +119,11 @@ services:
|
||||
- ./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
|
||||
@ -91,6 +133,11 @@ services:
|
||||
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
|
||||
@ -102,6 +149,225 @@ services:
|
||||
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).
|
||||
@ -593,8 +859,12 @@ services:
|
||||
|
||||
volumes:
|
||||
k3s-server-data:
|
||||
k3s-worker-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:
|
||||
|
||||
39
k3s/manifests/flux/bridge-gitea.yaml
Normal file
39
k3s/manifests/flux/bridge-gitea.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
# DNS-мост: имя `gitea` в namespace flux-system → compose-контейнер gitea.
|
||||
# Service без селектора + ручной Endpoints, по образцу bridge-postgres/bridge-minio.
|
||||
#
|
||||
# Зачем: source-controller Flux работает ВНУТРИ k3s, где резолвит CoreDNS, а имя
|
||||
# сервиса compose-сети там не существует. Без моста GitRepository падает с
|
||||
# "lookup gitea on 10.43.0.10:53: no such host".
|
||||
#
|
||||
# __GITEA_BRIDGE_IP__ подставляет контейнер flux-k8s-init из переменной
|
||||
# GITEA_BRIDGE_IP — она же задаёт ipv4_address сервиса gitea в docker-compose.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: flux-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: flux-system
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: flux-system
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: __GITEA_BRIDGE_IP__
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
protocol: TCP
|
||||
22
vault/config/vault.hcl
Normal file
22
vault/config/vault.hcl
Normal file
@ -0,0 +1,22 @@
|
||||
// Vault контура aero: file-хранилище на named volume, TLS терминируется выше
|
||||
// (верхний nginx). Внутри compose-сети слушаем открытым HTTP.
|
||||
// Инициализацию/распечатывание делает сервис vault-init (см. docker-compose.yaml).
|
||||
|
||||
ui = true
|
||||
|
||||
storage "file" {
|
||||
path = "/vault/file"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
tls_disable = true
|
||||
}
|
||||
|
||||
// Адрес, который Vault сообщает клиентам (и себе при редиректах внутри сети).
|
||||
api_addr = "http://vault:8200"
|
||||
|
||||
// mlock требует IPC_LOCK; в compose capability выдана, но в некоторых окружениях
|
||||
// (RedOS в контейнере без CAP_IPC_LOCK) Vault упадёт на старте. Отключаем —
|
||||
// swap на хосте контура и так выключен.
|
||||
disable_mlock = true
|
||||
Loading…
Reference in New Issue
Block a user