feat(aero): базовый слой контура — Vault↔k3s, GitOps-синхронизация, выход наружу

Топология k3s: 1 мастер + 2 воркера (убран k3s-worker-3 с томом).

Vault ↔ k3s. Vault живёт в compose, вне кластера, поэтому получает
статический IP 172.28.0.13, DNS-мост в namespace vault и token reviewer
(SA vault-auth + system:auth-delegator). Ansible включает auth/kubernetes,
передавая адрес apiserver, CA и JWT ревьюера явно, и заводит политику с
ролями. Секрет token reviewer'а читается целиком в JSON: вариант
-o jsonpath={.data.ca\.crt} НЕ работает — модуль command разбирает строку
через shlex, съедает обратный слэш, kubectl возвращает пустую строку с
кодом 0, и отказ остаётся незамеченным до падения vault write.

Синхронизация репозитория в gitea. Нужное подмножество путей (замыкание
ссылок clusters/aero) едет архивом на хост и коммитится там: gitea не
публикуется дальше самого хоста, контур остаётся замкнутым. flux-bootstrap
вынесен в отдельную волну — до неё Vault получает auth/kubernetes, а
репозиторий наполняется, иначе первая реконсиляция падает на пустом репо.

Инфраструктура: vault-agent-injector (чарт vault-contour в режиме внешнего
Vault), local-path-provisioner взамен встроенного в k3s (--disable=
local-storage; путь данных прежний), rabbitmq и minio с кредами из Vault.

Выход наружу: порты 80/443 k3s-server опубликованы (istio ingressgateway
занимает эти hostPort), общий contour-gateway на wildcard-хост, сертификат
Let's Encrypt через http01. Имена в сертификате перечислены явно —
http01 не выдаёт wildcard, для них нужен dns01.

Разорваны два дедлока Flux: infra-configs больше не зависит от
infra-controllers (издатели не должны зависеть от здоровья чартов, которые
их используют), istio-config получил disableWait.

Модули ядра iptable_nat и смежные грузятся на хосте: ноды k3s —
контейнеры, istio-init правит iptables через ядро хоста, а на RED OS 8
(nf_tables) legacy-модули не загружены, из-за чего любой под с
istio-injection навсегда вставал в Init:Error.

Собственные Gateway/VirtualService/Certificate чартов rabbitmq и dashboard
отключены — маршрутизация описана централизованно. У rabbitmq это сделано
postRenderers, а не values: kustomize нормализует null в {}, а пустую карту
Helm сливает с дефолтами, возвращая их целиком.
This commit is contained in:
emelinda 2026-08-08 09:36:08 +03:00
parent da8e40c044
commit 565faa0e17
27 changed files with 1184 additions and 33 deletions

View File

@ -2,6 +2,25 @@
# Каталог развёртывания на хосте (домашка root) # Каталог развёртывания на хосте (домашка root)
deploy_dir: /root/sarex deploy_dir: /root/sarex
# --- Модули ядра для istio-init --------------------------------------------
# Ноды k3s — это docker-контейнеры, своего ядра у них нет: любые операции с
# iptables внутри подов идут через ядро ХОСТА. istio-init настраивает перехват
# трафика и создаёт legacy-таблицу nat, поэтому на хосте должен быть загружен
# iptable_nat.
#
# На RED OS 8 (ядро 6.6, iptables в режиме nf_tables) он по умолчанию НЕ
# загружен: присутствуют только nft-варианты (nft_chain_nat, xt_nat). Без этих
# модулей istio-init падает с
# xtables parameter problem: iptables-restore: unable to initialize table 'nat'
# и КАЖДЫЙ под в namespace с istio-injection=enabled навсегда остаётся в
# Init:Error — именно так встал kubernetes-dashboard.
aero_kernel_modules:
- iptable_nat
- iptable_mangle
- iptable_raw
- xt_REDIRECT
- xt_owner
# Источник файлов — корень репозитория iac (playbook лежит в aero/) # Источник файлов — корень репозитория iac (playbook лежит в aero/)
deploy_src_root: "{{ playbook_dir }}/.." deploy_src_root: "{{ playbook_dir }}/.."
@ -58,6 +77,11 @@ registry_secrets:
- {name: regcred, namespace: cert-manager} - {name: regcred, namespace: cert-manager}
- {name: regcred, namespace: istio-system} - {name: regcred, namespace: istio-system}
- {name: regcred, namespace: kubernetes-dashboard} - {name: regcred, namespace: kubernetes-dashboard}
# Образы vault-k8s и vault-agent для injector'а тоже лежат в cr.yandex.
- {name: regcred, namespace: vault}
- {name: regcred, namespace: local-path-provisioner}
- {name: regcred, namespace: rabbitmq}
- {name: regcred, namespace: minio}
# Поднимать стек (docker compose up -d). Требует запущенной службы docker и # Поднимать стек (docker compose up -d). Требует запущенной службы docker и
# docker login в cr.yandex — по умолчанию выключено. # docker login в cr.yandex — по умолчанию выключено.
@ -75,6 +99,50 @@ gitea_repo: iac
aero_git_branch: master aero_git_branch: master
aero_flux_path: clusters/aero aero_flux_path: clusters/aero
# --- Синхронизация репозитория в gitea --------------------------------------
# Содержимое едет с control-node на хост архивом и коммитится в gitea оттуда:
# наружу gitea не публикуется дальше самого хоста, а git на control-node может
# быть не настроен на работу с контуром.
#
# Список — это ЗАМЫКАНИЕ ссылок clusters/aero, а не весь репозиторий:
# clusters/aero/controllers ссылается на ../../../infrastructure/<компонент>/aero,
# каждый такой overlay тянет только ../base. Каталоги ansible-роли, snapshots и
# docs кластеру не нужны и не синхронизируются.
#
# ВАЖНО: добавил компонент в clusters/aero/controllers — добавь его сюда, иначе
# kustomize в кластере упадёт на отсутствующем каталоге.
gitea_sync_paths:
- clusters/aero
- infrastructure/cert-manager
- infrastructure/dashboard
- infrastructure/istio-base
- infrastructure/istio-config
- infrastructure/istio-gateway
- infrastructure/istio-pilot
- infrastructure/vault-injector
- infrastructure/local-path-provisioner
- infrastructure/rabbitmq
- infrastructure/minio
# Рабочая копия репозитория gitea на хосте (клон, живёт между прогонами).
gitea_sync_workdir: "{{ deploy_dir }}/gitea-sync"
gitea_sync_archive_remote: "{{ deploy_dir }}/gitea-sync.tar.gz"
# Адрес gitea с точки зрения ХОСТА: контейнер публикует 3000 наружу.
gitea_http_addr: "http://127.0.0.1:3000"
# URL с кредами для push'а. sarex_secrets заполняется в tasks/main.yml, поэтому
# выражение раскрывается лениво — к моменту использования пароль уже есть.
gitea_sync_url: >-
http://{{ gitea_admin_user }}:{{ sarex_secrets.GITEA_ADMIN_PASSWORD | urlencode }}@127.0.0.1:3000/{{ gitea_org }}/{{ gitea_repo }}.git
# Временный архив на control-node (удаляется после синхронизации).
gitea_sync_archive_local: /tmp/aero-gitea-sync.tar.gz
# Автор коммитов синхронизации.
gitea_commit_name: aero-sync
gitea_commit_email: "aero-sync@{{ platform_domain }}"
# Поднимается отдельно от прикладного стека: образы публичные, docker login в # Поднимается отдельно от прикладного стека: образы публичные, docker login в
# cr.yandex не нужен. По умолчанию выключено, включает `uv run poe platform`. # cr.yandex не нужен. По умолчанию выключено, включает `uv run poe platform`.
aero_platform_up: false aero_platform_up: false
@ -85,17 +153,23 @@ aero_platform_services_wave1:
- k3s-server - k3s-server
- k3s-worker-1 - k3s-worker-1
- k3s-worker-2 - k3s-worker-2
- k3s-worker-3
- gitea - gitea
# Волна 2: провижининг и bootstrap. gitea-init, flux-k8s-init и flux-bootstrap # Волна 2: провижининг. gitea-init и flux-k8s-init одноразовые (restart: "no"),
# одноразовые (restart: "no"), vault-init живёт постоянно и распечатывает Vault # vault-init живёт постоянно и распечатывает Vault после ребута хоста.
# после ребута хоста. #
# flux-bootstrap ВЫНЕСЕН в волну 3 намеренно: между волнами Vault получает
# auth/kubernetes, а репозиторий контура синхронизируется в gitea. Запусти
# bootstrap раньше — Flux привязался бы к репозиторию, в котором ещё нет ни
# clusters/aero, ни infrastructure, и первая же реконсиляция упала бы.
aero_platform_services_wave2: aero_platform_services_wave2:
- gitea-init - gitea-init
- flux-k8s-init - flux-k8s-init
- vault - vault
- vault-init - vault-init
# Волна 3: bootstrap FluxCD. Запускается после наполнения gitea.
aero_platform_services_wave3:
- flux-bootstrap - flux-bootstrap
# Ноды k3s. Каждой нужен свой каталог PVC-хранилища на хосте: провижинер # Ноды k3s. Каждой нужен свой каталог PVC-хранилища на хосте: провижинер
@ -104,11 +178,57 @@ aero_k3s_nodes:
- server - server
- worker-1 - worker-1
- worker-2 - worker-2
- worker-3
# Сколько ждать появления kubeconfig от k3s-server, секунд. # Сколько ждать появления kubeconfig от k3s-server, секунд.
aero_kubeconfig_timeout: 180 aero_kubeconfig_timeout: 180
# --- Интеграция Vault ↔ k3s (метод auth/kubernetes) -------------------------
# Статический IP контейнера vault в compose-сети. ДОЛЖЕН совпадать с
# ipv4_address сервиса vault в docker-compose.yaml: отсюда значение
# подставляется в Endpoints DNS-моста (k3s/manifests/vault/bridge-vault.yaml).
vault_bridge_ip: 172.28.0.13
# Адрес apiserver, по которому Vault валидирует токены подов. Vault живёт в
# compose, поэтому имя резолвится compose-DNS, а не CoreDNS кластера.
vault_k8s_host: "https://k3s-server:6443"
# Политика чтения kv-v2 на пути secrets/ — том же, что включает vault-init.
vault_apps_policy: apps-read
# TTL выдаваемого агенту токена по умолчанию.
vault_role_ttl: 24h
# Роли auth/kubernetes. Имя роли — то, что приложение указывает в аннотации
# vault.hashicorp.com/role, namespace — где живут его поды. Пополняется по мере
# переезда приложений из compose в k3s.
vault_k8s_roles:
- {name: rabbitmq, namespaces: [rabbitmq]}
- {name: minio, namespaces: [minio]}
- {name: django, namespaces: [django]}
- {name: processing, namespaces: [processing]}
# Боевые креды инфраструктурных сервисов. Пути и имена ключей заданы чартами
# (см. vaultRoot у minio и auth.vault у rabbitmq в infrastructure/*/aero) —
# менять их в отрыве от values нельзя.
#
# Пароли не хранятся в репозитории: берутся из sarex_secrets, который
# генерирует и персистит в aero/.secrets задача "Сгенерировать/загрузить пароли".
vault_app_secrets:
- path: secrets/rabbitmq/auth
data:
username: "{{ sarex_rabbitmq_k8s_user }}"
password: "{{ sarex_secrets.SAREX_RABBITMQ_PASSWORD }}"
- path: secrets/minio/admin
data:
rootUser: "{{ sarex_minio_k8s_user }}"
rootPassword: "{{ sarex_secrets.SAREX_MINIO_ROOT_PASSWORD }}"
# Логины сервисов в k3s. Совпадают со значениями в .env.example для
# одноимённых сервисов compose; пароли к ним — в sarex_secrets (те же
# переменные: контур переезжает в k3s, плодить вторую пару кредов незачем).
sarex_rabbitmq_k8s_user: rabbit
sarex_minio_k8s_user: minioadmin
# Сервисы приложения, поднимаемые при sarex_compose_up (depends_on тянет # Сервисы приложения, поднимаемые при sarex_compose_up (depends_on тянет
# зависимости и порядок). k3s/gitea намеренно не поднимаем. # зависимости и порядок). k3s/gitea намеренно не поднимаем.
sarex_services: sarex_services:

View File

@ -0,0 +1,140 @@
---
# Синхронизация репозитория контура в gitea.
#
# Flux читает манифесты ИЗ gitea, а не из этого репозитория, поэтому содержимое
# нужно туда доставить. Схема: control-node пакует нужное подмножество путей в
# архив → ansible кладёт его на хост → на хосте архив распаковывается в рабочую
# копию репозитория gitea и коммитится.
#
# Почему коммит делается на хосте, а не push'ем с control-node: gitea публикует
# порт только на самом хосте, и контур остаётся замкнутым — наружу за git никто
# не ходит.
#
# Синхронизируется ЗАМЫКАНИЕ ссылок clusters/aero (gitea_sync_paths), а не весь
# репозиторий: ansible-роль, snapshots и docs кластеру не нужны.
#
# Идемпотентно: если содержимое не изменилось, коммита не будет.
- name: Установить git на хосте (нужен для коммита в gitea)
ansible.builtin.package:
name: git
state: present
- name: Проверить, что синхронизируемые пути существуют
ansible.builtin.stat:
path: "{{ deploy_src_root }}/{{ item }}"
loop: "{{ gitea_sync_paths }}"
loop_control:
label: "{{ item }}"
delegate_to: localhost
become: false
register: gitea_sync_stat
- name: Прервать деплой, если путь из gitea_sync_paths отсутствует
ansible.builtin.fail:
msg: >-
Путь {{ item.item }} указан в gitea_sync_paths, но отсутствует в
репозитории. Список должен совпадать с тем, на что ссылается
clusters/aero — иначе kustomize в кластере упадёт.
loop: "{{ gitea_sync_stat.results }}"
loop_control:
label: "{{ item.item }}"
when: not item.stat.exists
# Архив собирается из РАБОЧЕГО ДЕРЕВА, а не из HEAD: синхронизировать нужно то,
# что лежит на диске, включая ещё не закоммиченные локально правки.
- name: Собрать архив синхронизируемых путей на control-node
ansible.builtin.command:
cmd: "tar -czf {{ gitea_sync_archive_local }} {{ gitea_sync_paths | join(' ') }}"
chdir: "{{ deploy_src_root }}"
delegate_to: localhost
become: false
changed_when: true
- name: Запомнить исходную ревизию (для сообщения коммита)
ansible.builtin.command:
cmd: git rev-parse --short HEAD
chdir: "{{ deploy_src_root }}"
delegate_to: localhost
become: false
register: gitea_sync_rev
changed_when: false
failed_when: false
- name: Скопировать архив на хост
ansible.builtin.copy:
src: "{{ gitea_sync_archive_local }}"
dest: "{{ gitea_sync_archive_remote }}"
mode: "0600"
- name: Синхронизировать и закоммитить содержимое в gitea
ansible.builtin.shell:
cmd: |
set -eu
WORKDIR={{ gitea_sync_workdir | quote }}
BRANCH={{ aero_git_branch | quote }}
URL={{ gitea_sync_url | quote }}
mkdir -p "$WORKDIR"
cd "$WORKDIR"
# Клон может не существовать, а репозиторий в gitea — быть пустым (до
# первого коммита у него нет ни одной ветки). init+fetch переживает оба
# случая, в отличие от `git clone`, который на пустом репозитории
# оставляет рабочую копию без ветки.
[ -d .git ] || git init -q
git config user.name {{ gitea_commit_name | quote }}
git config user.email {{ gitea_commit_email | quote }}
git remote remove origin 2>/dev/null || true
git remote add origin "$URL"
if git fetch -q origin "$BRANCH" 2>/dev/null; then
git checkout -q -B "$BRANCH" FETCH_HEAD
else
git checkout -q -B "$BRANCH"
fi
# Управляемые пути удаляются перед распаковкой, иначе файлы, удалённые в
# исходном репозитории, остались бы в gitea навсегда: `git add -A` увидит
# удаление только если файла действительно нет на диске.
for p in {{ gitea_sync_paths | join(' ') }}; do
rm -rf "$p"
done
tar -xzf {{ gitea_sync_archive_remote | quote }}
git add -A
if git diff --cached --quiet; then
echo "SYNC_RESULT=nochange"
else
git commit -q -m "sync from control-node ({{ gitea_sync_rev.stdout | default('unknown') }})"
echo "SYNC_RESULT=committed"
fi
git push -q origin "$BRANCH"
echo "SYNC_DONE"
chdir: "{{ deploy_dir }}"
register: gitea_sync_result
changed_when: "'SYNC_RESULT=committed' in gitea_sync_result.stdout"
no_log: true
- name: Удалить архив с хоста
ansible.builtin.file:
path: "{{ gitea_sync_archive_remote }}"
state: absent
- name: Удалить архив с control-node
ansible.builtin.file:
path: "{{ gitea_sync_archive_local }}"
state: absent
delegate_to: localhost
become: false
- name: Итог синхронизации
ansible.builtin.debug:
msg: >-
gitea {{ gitea_org }}/{{ gitea_repo }}@{{ aero_git_branch }}:
{{ 'закоммичены изменения' if 'SYNC_RESULT=committed' in gitea_sync_result.stdout
else 'изменений нет' }}
(источник {{ gitea_sync_rev.stdout | default('unknown') }},
путей: {{ gitea_sync_paths | length }})

View File

@ -1,4 +1,17 @@
--- ---
# --- Предпосылки на хосте -------------------------------------------------
# Загружаем ДО подъёма k3s: istio-init внутри подов правит iptables через ядро
# хоста, и без этих модулей поды в namespace с istio-injection не стартуют.
# persistent: present закрепляет загрузку в /etc/modules-load.d, иначе после
# перезагрузки хоста контур снова развалился бы.
- name: Загрузить модули ядра, нужные istio-init
community.general.modprobe:
name: "{{ item }}"
state: present
persistent: present
loop: "{{ aero_kernel_modules }}"
tags: [kernel]
# --- Каталоги и файлы ---------------------------------------------------- # --- Каталоги и файлы ----------------------------------------------------
- name: Создать каталог развёртывания - name: Создать каталог развёртывания
ansible.builtin.file: ansible.builtin.file:

View File

@ -33,12 +33,44 @@
- name: Создать секреты доступа к реестру в кластере - name: Создать секреты доступа к реестру в кластере
ansible.builtin.include_tasks: registry-secrets.yml ansible.builtin.include_tasks: registry-secrets.yml
- name: Волна 2 — gitea-init, vault(+init) и bootstrap Flux - name: Волна 2 — gitea-init, flux-k8s-init и vault(+init)
ansible.builtin.command: ansible.builtin.command:
cmd: "{{ compose_cmd }} up -d {{ aero_platform_services_wave2 | join(' ') }}" cmd: "{{ compose_cmd }} up -d {{ aero_platform_services_wave2 | join(' ') }}"
chdir: "{{ deploy_dir }}" chdir: "{{ deploy_dir }}"
changed_when: true changed_when: true
# vault-init инициализирует и распечатывает Vault в цикле с шагом 10 секунд,
# поэтому сразу после `up -d` root-токена ещё нет. Он нужен следующему шагу,
# который настраивает auth/kubernetes.
- name: Дождаться инициализации Vault (появления root-токена)
ansible.builtin.command:
cmd: "{{ compose_cmd }} exec -T vault-init test -s /vault/init/root.token"
chdir: "{{ deploy_dir }}"
register: aero_vault_ready
until: aero_vault_ready.rc == 0
retries: 30
delay: 5
changed_when: false
# Метод auth/kubernetes: Vault учится валидировать токены подов k3s, а поды
# получают секреты файлами через vault-agent-injector (ставится Flux'ом).
- name: Настроить интеграцию Vault с k3s
ansible.builtin.include_tasks: vault-k8s.yml
tags: [vault]
# Наполнение gitea — ДО bootstrap: Flux начинает реконсиляцию сразу после
# привязки, и репозиторий к этому моменту должен содержать clusters/aero
# вместе со всем, на что тот ссылается.
- name: Синхронизировать репозиторий контура в gitea
ansible.builtin.include_tasks: gitea-sync.yml
tags: [sync]
- name: Волна 3 — bootstrap Flux
ansible.builtin.command:
cmd: "{{ compose_cmd }} up -d {{ aero_platform_services_wave3 | join(' ') }}"
chdir: "{{ deploy_dir }}"
changed_when: true
# flux-bootstrap одноразовый: успех = код 0. Забираем его явно, иначе ошибка # flux-bootstrap одноразовый: успех = код 0. Забираем его явно, иначе ошибка
# bootstrap'а останется незамеченной — `compose up -d` о ней не сообщает. # bootstrap'а останется незамеченной — `compose up -d` о ней не сообщает.
# #
@ -85,4 +117,8 @@
msg: msg:
- "gitea: http://{{ ansible_host }}:3000 (пользователь {{ gitea_admin_user }}, пароль в aero/.secrets/{{ inventory_hostname }}/GITEA_ADMIN_PASSWORD)" - "gitea: http://{{ ansible_host }}:3000 (пользователь {{ gitea_admin_user }}, пароль в aero/.secrets/{{ inventory_hostname }}/GITEA_ADMIN_PASSWORD)"
- "Flux реконсилирует {{ gitea_org }}/{{ gitea_repo }} → {{ aero_flux_path }}" - "Flux реконсилирует {{ gitea_org }}/{{ gitea_repo }} → {{ aero_flux_path }}"
- "Проверка: docker compose exec k3s-server kubectl -n flux-system get kustomizations" - "Слои: infra-controllers (istio, cert-manager, vault-injector, local-path, rabbitmq, minio) → infra-configs → apps"
- "Состояние Flux: docker compose exec k3s-server kubectl -n flux-system get kustomizations,helmreleases -A"
- "Dashboard: https://dashboard.{{ platform_domain }}"
- "RabbitMQ: https://rabbitmq.{{ platform_domain }} (пользователь {{ sarex_rabbitmq_k8s_user }}, пароль в aero/.secrets/{{ inventory_hostname }}/SAREX_RABBITMQ_PASSWORD)"
- "MinIO UI: https://minio.{{ platform_domain }}/console/ (пользователь {{ sarex_minio_k8s_user }}, пароль в aero/.secrets/{{ inventory_hostname }}/SAREX_MINIO_ROOT_PASSWORD)"

View File

@ -0,0 +1,169 @@
---
# Интеграция Vault ↔ k3s: метод auth/kubernetes.
#
# Vault контура живёт в docker-compose, ВНЕ кластера, поэтому не может
# воспользоваться ни собственным ServiceAccount-токеном, ни CA из
# /var/run/secrets — всё это ему нужно передать явно:
# kubernetes_host — адрес apiserver, резолвится по compose-DNS;
# kubernetes_ca_cert — CA кластера (из секрета token reviewer'а);
# token_reviewer_jwt — долгоживущий токен SA vault-auth.
#
# После этого под с аннотациями vault.hashicorp.com/* получает от injector'а
# init-контейнер vault-agent, тот логинится в Vault токеном ServiceAccount'а
# пода, Vault валидирует его через TokenReview и отдаёт секреты файлами
# в /vault/secrets/.
#
# Все шаги идемпотентны: повторный прогон на настроенном Vault — no-op.
#
# ВНИМАНИЕ по стилю: команды здесь задаются ЛИТЕРАЛЬНЫМ блоком (|), а не
# складывающим (>-). В складывающем скаляре строка с бОльшим отступом считается
# "more-indented", и YAML СОХРАНЯЕТ перед ней перевод строки — многострочная
# команда разрывается посреди аргументов. Именно на этом падал первый вариант:
# `vault write auth/kubernetes/config` отрывался от своих параметров.
- name: Применить манифесты vault в k3s (DNS-мост + token reviewer)
ansible.builtin.shell:
cmd: |
{{ compose_cmd }} exec -T k3s-server sh -ec '
sed "s|__VAULT_BRIDGE_IP__|{{ vault_bridge_ip }}|g" /output/manifests/vault/bridge-vault.yaml |
kubectl apply -f -
kubectl apply -f /output/manifests/vault/vault-auth.yaml
'
chdir: "{{ deploy_dir }}"
changed_when: true
# Секрет читается ЦЕЛИКОМ в JSON, а поля достаются фильтром from_json.
#
# Так сделано не из вкуса: вариант `-o jsonpath={.data.ca\.crt}` здесь НЕ
# работает. Модуль command разбирает строку через shlex с обработкой escape-
# последовательностей и съедает обратный слэш, поэтому kubectl получает
# {.data.ca.crt} — ищет вложенное поле crt внутри ca, не находит и возвращает
# ПУСТУЮ строку с кодом 0. Отказ тихий: задача чтения проходит успешно, а
# падает уже `vault write` с пустым CA. Ключ token при этом читался верно —
# в нём нет точки, — что делало симптом ещё запутаннее.
#
# token-controller заполняет секрет асинхронно, поэтому ждём оба поля.
- name: Прочитать секрет token reviewer'а (дождавшись заполнения)
ansible.builtin.command:
cmd: >-
{{ compose_cmd }} exec -T k3s-server
kubectl -n vault get secret vault-auth-token -o json
chdir: "{{ deploy_dir }}"
register: vault_auth_secret
until: >-
vault_auth_secret.rc == 0
and '"ca.crt"' in vault_auth_secret.stdout
and '"token"' in vault_auth_secret.stdout
retries: 30
delay: 2
changed_when: false
no_log: true
- name: Выделить CA и JWT из секрета
ansible.builtin.set_fact:
vault_k8s_ca_b64: "{{ (vault_auth_secret.stdout | from_json).data['ca.crt'] }}"
vault_sa_token_b64: "{{ (vault_auth_secret.stdout | from_json).data['token'] }}"
no_log: true
- name: Проверить, что CA и JWT непустые
ansible.builtin.assert:
that:
- vault_k8s_ca_b64 | length > 0
- vault_sa_token_b64 | length > 0
fail_msg: >-
Секрет vault-auth-token прочитан, но CA или JWT пусты — Vault нельзя
настроить на валидацию токенов подов.
success_msg: "CA и JWT получены"
# Повторный вызов на уже включённом пути отдаёт "path is already in use"
# и ненулевой код — это штатно, глушим.
- name: Включить метод auth/kubernetes в Vault
ansible.builtin.shell:
cmd: |
{{ compose_cmd }} exec -T vault-init sh -ec '
export VAULT_TOKEN=$(cat /vault/init/root.token)
vault auth enable kubernetes 2>&1 | grep -v "path is already in use" || true
'
chdir: "{{ deploy_dir }}"
changed_when: true
# CA и JWT передаём внутрь контейнера base64-строкой и раскодируем во временные
# файлы: vault CLI умеет читать значение параметра из файла синтаксисом @путь,
# и так не приходится экранировать многострочный PEM в аргументах.
- name: Настроить auth/kubernetes (адрес apiserver, CA, token reviewer)
ansible.builtin.shell:
cmd: |
{{ compose_cmd }} exec -T vault-init sh -ec '
export VAULT_TOKEN=$(cat /vault/init/root.token)
umask 077
echo "{{ vault_k8s_ca_b64 }}" | base64 -d > /tmp/k8s-ca.crt
echo "{{ vault_sa_token_b64 }}" | base64 -d > /tmp/k8s-jwt
vault write auth/kubernetes/config kubernetes_host="{{ vault_k8s_host }}" kubernetes_ca_cert=@/tmp/k8s-ca.crt token_reviewer_jwt=@/tmp/k8s-jwt
rm -f /tmp/k8s-ca.crt /tmp/k8s-jwt
'
chdir: "{{ deploy_dir }}"
changed_when: true
no_log: true
# Политика доступа к kv-v2 на пути secrets/ — том самом, что включает vault-init
# и на который ссылаются аннотации приложений (secrets/data/apps/<app>/...).
- name: Создать политику чтения секретов
ansible.builtin.shell:
cmd: |
{{ compose_cmd }} exec -T vault-init sh -ec '
export VAULT_TOKEN=$(cat /vault/init/root.token)
vault policy write {{ vault_apps_policy }} - <<EOF
path "secrets/data/*" {
capabilities = ["read"]
}
path "secrets/metadata/*" {
capabilities = ["read", "list"]
}
EOF
'
chdir: "{{ deploy_dir }}"
changed_when: true
# Роль на приложение: связывает ServiceAccount'ы в namespace с политикой.
# Имя роли — то, что приложение указывает в vault.hashicorp.com/role.
- name: Создать роли auth/kubernetes для приложений
ansible.builtin.shell:
cmd: |
{{ compose_cmd }} exec -T vault-init sh -ec '
export VAULT_TOKEN=$(cat /vault/init/root.token)
vault write auth/kubernetes/role/{{ item.name }} bound_service_account_names="{{ item.service_accounts | default(["*"]) | join(",") }}" bound_service_account_namespaces="{{ item.namespaces | join(",") }}" policies="{{ item.policies | default([vault_apps_policy]) | join(",") }}" ttl={{ item.ttl | default(vault_role_ttl) }}
'
chdir: "{{ deploy_dir }}"
loop: "{{ vault_k8s_roles }}"
loop_control:
label: "{{ item.name }}"
changed_when: true
# Значения передаём JSON'ом через stdin, а не парами key=value в аргументах:
# так не приходится экранировать пробелы и спецсимволы внутри вложенных кавычек.
#
# kv-v2 версионирует записи, поэтому повторный прогон создаёт новую версию с тем
# же содержимым. Для демо-секрета это безобидно; боевые секреты сюда не кладутся.
- name: Положить креды сервисов в Vault
ansible.builtin.shell:
cmd: |
{{ compose_cmd }} exec -T vault-init sh -ec '
export VAULT_TOKEN=$(cat /vault/init/root.token)
vault kv put {{ item.path }} - <<EOF
{{ item.data | to_json }}
EOF
'
chdir: "{{ deploy_dir }}"
loop: "{{ vault_app_secrets }}"
loop_control:
label: "{{ item.path }}"
changed_when: true
# Пароли сервисов не должны попасть в лог.
no_log: true
- name: Итог интеграции Vault ↔ k3s
ansible.builtin.debug:
msg:
- "auth/kubernetes настроен на {{ vault_k8s_host }}"
- "Роли: {{ vault_k8s_roles | map(attribute='name') | join(', ') }}"
- "Проверка ролей: docker compose exec vault-init sh -c 'VAULT_TOKEN=$(cat /vault/init/root.token) vault list auth/kubernetes/role'"

24
clusters/aero/apps.yaml Normal file
View File

@ -0,0 +1,24 @@
# Слой приложений. Отделён от инфраструктуры по той же причине, что controllers
# от configs (см. infrastructure.yaml): приложениям нужны уже готовые CRD и
# работающие вебхуки.
#
# dependsOn: infra-controllers ставит istiod и vault-agent-injector, а под
# тестового приложения без них не поднимется — MutatingWebhook инжектора
# перехватывает создание пода, и пока вебхук не готов, под не создастся.
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 10m0s
path: ./clusters/aero/apps
prune: true
wait: true
timeout: 10m0s
dependsOn:
- name: infra-controllers
sourceRef:
kind: GitRepository
name: flux-system

View File

@ -0,0 +1,15 @@
# Слой 3: прикладные сервисы контура.
#
# Сейчас пуст. Тестовое приложение (apps/hello) удалено за ненадобностью: оно
# существовало, чтобы доказать работоспособность подложки — доставку Flux'ом,
# внедрение sidecar istio и получение секрета из Vault. Теперь это же
# подтверждают боевые сервисы: rabbitmq и minio поднимаются с кредами,
# приезжающими из Vault через vault-agent-injector.
#
# Слой намеренно оставлен пустым, а не удалён: следующий этап — перенос
# прикладных сервисов из compose в k3s, и они приедут именно сюда. Пустой
# resources корректен для kustomize; Flux при этом удалит из кластера то, что
# раньше применял этот слой (prune: true).
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: []

View File

@ -4,15 +4,25 @@
# Порядок внутри слоя обеспечивают dependsOn в самих HelmRelease: # Порядок внутри слоя обеспечивают dependsOn в самих HelmRelease:
# istiod и ingressgateway ждут istio-base, istio-config ждёт всех троих. # istiod и ingressgateway ждут istio-base, istio-config ждёт всех троих.
# #
# local-path-provisioner намеренно НЕ подключён: в k3s он встроен и уже # local-path-provisioner подключён ЯВНО, а встроенный в k3s — отключён флагом
# обслуживает StorageClass local-path (по умолчанию). Чарт из репозитория # --disable=local-storage (см. docker-compose.yaml). Два провижинера с одним
# создал бы второй провижинер с тем же именем класса. # именем StorageClass дрались бы за одни и те же PVC, поэтому ровно один из них
# должен быть активен. Каталог данных не изменился: чарту задан тот же путь
# /var/lib/rancher/k3s/storage, что смонтирован с хоста.
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
# Хранилище идёт первым: PVC rabbitmq и minio без StorageClass не создадутся.
- ../../../infrastructure/local-path-provisioner/aero
- ../../../infrastructure/cert-manager/aero - ../../../infrastructure/cert-manager/aero
- ../../../infrastructure/istio-base/aero - ../../../infrastructure/istio-base/aero
- ../../../infrastructure/istio-pilot/aero - ../../../infrastructure/istio-pilot/aero
- ../../../infrastructure/istio-gateway/aero - ../../../infrastructure/istio-gateway/aero
- ../../../infrastructure/istio-config/aero - ../../../infrastructure/istio-config/aero
- ../../../infrastructure/dashboard/aero - ../../../infrastructure/dashboard/aero
# Только Vault Agent Injector: сам Vault живёт в compose, а не в кластере.
- ../../../infrastructure/vault-injector/aero
# Брокер и объектное хранилище. Креды берут из Vault через injector, поэтому
# идут после него. Админки выставлены наружу через istio-config/aero.
- ../../../infrastructure/rabbitmq/aero
- ../../../infrastructure/minio/aero

View File

@ -7,8 +7,29 @@
# и не применяется НИЧЕГО — включая сам cert-manager. Дедлок: CRD никогда не # и не применяется НИЧЕГО — включая сам cert-manager. Дедлок: CRD никогда не
# появятся, потому что слой не может пройти проверку. # появятся, потому что слой не может пройти проверку.
# #
# Поэтому: controllers ставят чарты (и CRD), configs применяются после них # Поэтому: controllers ставят чарты (и CRD), configs содержат только
# через dependsOn и содержат только пользовательские ресурсы. # пользовательские ресурсы.
#
# ПОЧЕМУ У infra-configs НЕТ dependsOn (было — и создавало дедлок).
#
# В configs лежат ClusterIssuer'ы. Чарты из слоя controllers создают
# Certificate, а Flux после установки релиза ждёт готовности его ресурсов.
# С dependsOn получался круг:
# чарт с Certificate ждёт выпуска
# → выпуск ждёт ClusterIssuer
# → ClusterIssuer в configs
# → configs ждёт готовности controllers
# → а там висит тот самый чарт
# Наступали на это дважды: сначала istio-config, следом rabbitmq (тот провисел
# в "Running 'install' action" полчаса).
#
# Жёсткий порядок заменён сходимостью через повторы: пока cert-manager не
# поставил CRD, configs будет падать на dry-run и переприменяться каждые
# 10 минут, а как только CRD появятся — применится сам. Издатели при этом
# больше не зависят от здоровья чартов, которые в них нуждаются.
#
# apps по-прежнему ждёт controllers, и это правильно: без istiod и
# vault-agent-injector его поды физически не поднимутся.
--- ---
apiVersion: kustomize.toolkit.fluxcd.io/v1 apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization kind: Kustomization
@ -34,8 +55,9 @@ spec:
interval: 10m0s interval: 10m0s
path: ./clusters/aero/configs path: ./clusters/aero/configs
prune: true prune: true
dependsOn: # dependsOn намеренно отсутствует — см. комментарий в начале файла.
- name: infra-controllers # Повторные попытки до появления CRD дешевле, чем дедлок.
retryInterval: 1m0s
sourceRef: sourceRef:
kind: GitRepository kind: GitRepository
name: flux-system name: flux-system

View File

@ -5,3 +5,5 @@ resources:
- ./helm-repositories.yaml - ./helm-repositories.yaml
# Два слоя инфраструктуры с dependsOn — см. комментарий внутри файла. # Два слоя инфраструктуры с dependsOn — см. комментарий внутри файла.
- ./infrastructure.yaml - ./infrastructure.yaml
# Слой приложений. Пока пуст — сюда приедут сервисы при переносе из compose.
- ./apps.yaml

View File

@ -37,6 +37,12 @@ services:
- --tls-san=k3s-server - --tls-san=k3s-server
- --tls-san=${K3S_API_HOST:-127.0.0.1} # внешний IP/домен для доступа к apiserver извне - --tls-san=${K3S_API_HOST:-127.0.0.1} # внешний IP/домен для доступа к apiserver извне
- --disable=traefik - --disable=traefik
# Встроенный в k3s local-path-provisioner отключён: его заменяет чарт
# local-path-provisioner из репозитория (clusters/aero/controllers).
# Два провижинера с одним именем StorageClass дрались бы за одни и те же
# PVC. Каталог данных при этом НЕ меняется — чарту задан тот же путь
# /var/lib/rancher/k3s/storage, который смонтирован с хоста ниже.
- --disable=local-storage
privileged: true privileged: true
cgroup: host # kubelet в cgroup v2 (Ubuntu 22.04+/RedOS) cgroup: host # kubelet в cgroup v2 (Ubuntu 22.04+/RedOS)
restart: unless-stopped restart: unless-stopped
@ -44,10 +50,19 @@ services:
K3S_TOKEN: ${K3S_TOKEN:-supersecrettoken} K3S_TOKEN: ${K3S_TOKEN:-supersecrettoken}
K3S_KUBECONFIG_OUTPUT: /output/kubeconfig.yaml K3S_KUBECONFIG_OUTPUT: /output/kubeconfig.yaml
K3S_KUBECONFIG_MODE: "644" K3S_KUBECONFIG_MODE: "644"
#ports: ports:
# - "6443:6443" # kube-apiserver — доступен извне; ОБЯЗАТЕЛЬНО ограничь фаерволом до доверенных IP # Под istio ingressgateway занимает hostPort 80/443 ВНУТРИ этого
# - "80:80" # ingress http — задействуются после установки ingress-контроллера # контейнера и через nodeAffinity живёт только на control-plane, то есть
# - "443:443" # ingress https # именно здесь. Публикация этих портов наружу — единственное, что отделяет
# сервисы контура от внешнего мира; маршрутизация дальше по домену задаётся
# в infrastructure/istio-config/aero (Gateway + VirtualService).
- "80:80" # ingress http
- "443:443" # ingress https
# kube-apiserver наружу НЕ публикуем: доступ к нему — только изнутри
# compose-сети (engine ходит по KUBE_ADDR=https://k3s-server:6443).
# Если понадобится внешний kubectl — раскомментируй и ОБЯЗАТЕЛЬНО
# ограничь фаерволом до доверенных адресов.
# - "6443:6443"
volumes: volumes:
- k3s-server-data:/var/lib/rancher/k3s # etcd/state, образы, манифесты — только named volume - k3s-server-data:/var/lib/rancher/k3s # etcd/state, образы, манифесты — только named volume
- ./k3s:/output:z # отсюда забираем контекст k8s (:z — SELinux label для RedOS) - ./k3s:/output:z # отсюда забираем контекст k8s (:z — SELinux label для RedOS)
@ -97,15 +112,6 @@ services:
- ./k3s-storage/worker-2:/var/lib/rancher/k3s/storage:z - ./k3s-storage/worker-2:/var/lib/rancher/k3s/storage:z
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,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: namespace processing + DNS-мост postgres/minio.
# Образ k3s содержит kubectl; server/tls перекрываем на k3s-server:6443 # Образ k3s содержит kubectl; server/tls перекрываем на k3s-server:6443
# (kubeconfig из /output даёт клиентские креды, адрес/TLS переопределяем флагами). # (kubeconfig из /output даёт клиентские креды, адрес/TLS переопределяем флагами).
@ -242,6 +248,12 @@ services:
timeout: 5s timeout: 5s
retries: 30 retries: 30
start_period: 10s 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. Живёт постоянно: после рестарта
# хоста Vault поднимается запечатанным, и цикл распечатывает его сам. # хоста Vault поднимается запечатанным, и цикл распечатывает его сам.
@ -868,7 +880,6 @@ volumes:
k3s-server-data: k3s-server-data:
k3s-worker-1-data: k3s-worker-1-data:
k3s-worker-2-data: k3s-worker-2-data:
k3s-worker-3-data:
gitea-data: gitea-data:
sarex-vault-data: # file-хранилище Vault sarex-vault-data: # file-хранилище Vault
sarex-vault-init: # unseal-ключ и root-токен (только для vault-init) sarex-vault-init: # unseal-ключ и root-токен (только для vault-init)

View File

@ -0,0 +1,47 @@
# Боевой издатель контура: Let's Encrypt через ACME.
#
# Раньше здесь был только внутренний CA (clusterissuer-ca.yaml) — исходили из
# того, что контур закрытый и до ACME-сервера не достучаться. Это оказалось не
# так: домены контура резолвятся в публичном DNS
# sarex.local.lonsdaleites.ru → 158.160.38.249
# *.sarex.local.lonsdaleites.ru → 158.160.38.249
# а порты 80/443 хоста опубликованы наружу (см. docker-compose.yaml, k3s-server).
# Значит http01-проверка проходит, и сертификаты можно брать настоящие.
#
# ВАЖНО ПРО WILDCARD: решатель http01 принципиально НЕ выдаёт wildcard-
# сертификаты — Let's Encrypt требует для них dns01. Поэтому сертификат контура
# перечисляет имена явно (см. infrastructure/istio-config/aero). Wildcard
# остался только в hosts самого Gateway, где сертификат не нужен.
#
# Внутренний CA (aero-ca-issuer) НЕ удалён: он остаётся запасным вариантом на
# случай, когда ACME недоступен, и для внутренних имён, которых нет в публичном
# DNS.
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer-istio
spec:
acme:
# На этот адрес Let's Encrypt шлёт предупреждения об истечении.
email: "emelinda1990@gmail.com"
# ОТДЕЛЬНОЕ имя секрета, НЕ общепринятое в репозитории letsencrypt-secret-key.
#
# Тот секрет создаёт сам чарт cert-manager-infra (метки managed-by=Helm,
# аннотация helm.sh/resource-policy: keep) и кладёт в поле tls.key
# ЗАГЛУШКУ — невалидный PEM. cert-manager читает её как ключ ACME-аккаунта
# и падает:
# Account private key is invalid: error decoding private key PEM block
# Reason: ErrVerifyACMEAccount
# Удалить заглушку нельзя: чарт пересоздаст её на следующем апгрейде и
# затрёт настоящий ключ аккаунта. Поэтому издателю выдан собственный
# секрет — его создаёт и владеет им cert-manager, Helm его не трогает.
privateKeySecretRef:
name: letsencrypt-issuer-istio-account-key
server: "https://acme-v02.api.letsencrypt.org/directory"
solvers:
# class istio — challenge-Ingress обслуживает istio-ingressgateway,
# тот самый, что занимает hostPort 80/443 на k3s-server.
- http01:
ingress:
class: istio

View File

@ -4,4 +4,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
# Внутренний CA — запасной издатель для имён, которых нет в публичном DNS.
- clusterissuer-ca.yaml - clusterissuer-ca.yaml
# Боевой издатель: Let's Encrypt, http01 через istio-ingressgateway.
- clusterissuer-letsencrypt.yaml

View File

@ -0,0 +1,38 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: dashboard
namespace: kubernetes-dashboard
spec:
interval: 5m
timeout: 10m
values:
# Kong слушает 8000 открытым HTTP и ничего не знает про istio. Без этого
# DestinationRule sidecar инициирует к нему mTLS, Kong видит сырое TLS-
# рукопожатие на плейнтекстовом порту, отвечает 400 — а наружу istio отдаёт
# 503. В логах Kong это выглядит как "\x16\x03\x01..." с SNI
# outbound_.80_._.dashboard-kong-proxy...
# tlsMode: DISABLE переводит обращение к нему в обычный HTTP.
destinationRule:
enabled: true
host: "dashboard-kong-proxy"
tlsMode: "DISABLE"
# Собственные VirtualService и Gateway чарта выключены: маршрутизация контура
# описана централизованно в infrastructure/istio-config/aero. Плюс родной VS
# чарта всё равно нерабочий — он ссылается на сервис
# kubernetes-dashboard-kong-proxy, которого не существует (реальное имя —
# dashboard-kong-proxy), и на домен dashboard.preprod.sarex.io, к контуру
# отношения не имеющий.
virtualService:
enabled: false
gateway:
enabled: false
# Образы лежат в приватном cr.yandex.
app:
image:
pullSecrets:
- regcred
kong:
image:
pullSecrets:
- regcred

View File

@ -3,3 +3,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
resources: resources:
- ../base - ../base
patches:
- path: dashboard.yaml
target:
kind: HelmRelease
name: dashboard

View File

@ -6,6 +6,25 @@ metadata:
spec: spec:
interval: 5m interval: 5m
timeout: 10m timeout: 10m
# disableWait РАЗРЫВАЕТ ДЕДЛОК, не украшательство.
#
# Чарт создаёт Certificate, а Flux после установки релиза ждёт готовности его
# ресурсов — в том числе этого сертификата. Выпустить его нельзя, пока нет
# ClusterIssuer, а издатели живут в слое infra-configs, который по dependsOn
# ждёт готовности infra-controllers, где и находится этот самый релиз. Круг:
# istio-config → Certificate → ClusterIssuer → infra-configs
# → infra-controllers → istio-config
# На практике это выглядело как вечное "Running 'upgrade' action" при
# Released: True и Certificate в состоянии
# "Issuing certificate as Secret does not exist".
#
# Ждать здесь всё равно нечего: чарт разворачивает только конфигурационные
# объекты (Gateway, VirtualService, Certificate), а не рабочие нагрузки.
# Сертификат выпустится асинхронно, как только появится издатель.
install:
disableWait: true
upgrade:
disableWait: true
# Список продублирован из base целиком: для CRD kustomize заменяет списки, # Список продублирован из base целиком: для CRD kustomize заменяет списки,
# а не сливает, поэтому частичный патч потерял бы зависимости от istio. # а не сливает, поэтому частичный патч потерял бы зависимости от istio.
# cert-manager добавлен сверх base — чарт istio-config рендерит Certificate, # cert-manager добавлен сверх base — чарт istio-config рендерит Certificate,
@ -28,10 +47,82 @@ spec:
# Блок обязателен, даже пустой: шаблоны gateway.yaml и virtualservice.yaml # Блок обязателен, даже пустой: шаблоны gateway.yaml и virtualservice.yaml
# разыменовывают $env.istio.gateways / .virtualServices без проверки на # разыменовывают $env.istio.gateways / .virtualServices без проверки на
# nil и падают с "nil pointer evaluating interface {}". # nil и падают с "nil pointer evaluating interface {}".
# Публикацию сервисов наружу добавим отдельным шагом.
istio: istio:
gateways: {} # Один общий Gateway на весь контур вместо отдельного на каждый сервис.
virtualServices: {} # Так сделано потому, что у контура есть wildcard-домен: сертификат и
# Gateway покрывают *.sarex.local.lonsdaleites.ru целиком, а новый
# сервис публикуется добавлением ОДНОГО VirtualService — без правки
# сертификатов и Gateway. В прод-кластерах домены выписаны поштучно,
# там wildcard'а нет.
gateways:
contour:
name: contour-gateway
namespace: gateway
servers:
# ПОРЯДОК ЗНАЧИМ: первым обязан идти хост без звёздочки.
# Чарт собирает из hosts[0] имя порта
# name: {{ printf "%s-https-443" (index $server.hosts 0 ...) }}
# и НЕ заключает результат в кавычки. Хост "*.domain" дал бы
# токен "*-domain-https-443", который YAML читает как ссылку на
# якорь, и Helm падает с
# MalformedYAMLError: yaml: unknown anchor ... referenced
# Сами hosts чарт квотирует, поэтому wildcard ниже безопасен.
- hosts:
- sarex.local.lonsdaleites.ru
- "*.sarex.local.lonsdaleites.ru"
tls:
credentialName: contour-tls
# ВНИМАНИЕ: имя ресурса VirtualService чарт берёт из КЛЮЧА map
# ({{ $vsName }}), а поле name внутри — игнорирует, в отличие от
# gateways, где .name учитывается. Поэтому здесь его нет: ключ и есть
# имя. В остальных кластерах репозитория name у virtualServices
# присутствует, но ни на что не влияет.
virtualServices:
# Kubernetes Dashboard. Свой VirtualService чарт дашборда создаёт на
# домен dashboard.preprod.sarex.io — он к контуру aero отношения не
# имеет и никуда не резолвится. Этот выводит дашборд на домен
# контура; точка входа — kong-proxy, как во всех остальных кластерах.
dashboard:
namespace: gateway
hosts:
- dashboard.sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
- path:
prefix: /
service: dashboard-kong-proxy.kubernetes-dashboard.svc.cluster.local
port: 80
# Management UI RabbitMQ.
rabbitmq:
namespace: gateway
hosts:
- rabbitmq.sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
- path:
prefix: /
service: rabbitmq.rabbitmq.svc.cluster.local
port: 15672
# MinIO: консоль на /console/, S3 API в корне. Порядок маршрутов
# значим — более специфичный префикс должен идти первым, иначе его
# перехватит правило для /.
minio:
namespace: gateway
hosts:
- minio.sarex.local.lonsdaleites.ru
gateways:
- gateway/contour-gateway
routes:
- path:
prefix: /console/
service: minio-console.minio.svc.cluster.local
port: 9001
- path:
prefix: /
service: minio.minio.svc.cluster.local
port: 9000
requestAuthentications: {} requestAuthentications: {}
authorizationPolicies: {} authorizationPolicies: {}
certManager: certManager:
@ -43,9 +134,23 @@ spec:
# Домены продублированы из aero/roles/sarex_stack/defaults/main.yml # Домены продублированы из aero/roles/sarex_stack/defaults/main.yml
# (platform_domain и производные). Единого источника нет — так же # (platform_domain и производные). Единого источника нет — так же
# захардкожено во всех остальных кластерах репозитория. # захардкожено во всех остальных кластерах репозитория.
dashboard-tls: #
# Один сертификат на весь контур, общий для всех его сервисов.
contour-tls:
# Имена перечислены ЯВНО, а не wildcard'ом: решатель http01
# принципиально не выдаёт wildcard-сертификаты, для них Let's
# Encrypt требует dns01, а webhook DNS-провайдера в контуре не
# настроен. Wildcard остался в hosts Gateway — там он про
# маршрутизацию, а не про TLS.
#
# Публикуешь новый сервис — добавь его имя сюда, иначе браузер
# получит сертификат, в котором этого имени нет.
dnsNames: dnsNames:
- sarex.local.lonsdaleites.ru
- dashboard.sarex.local.lonsdaleites.ru - dashboard.sarex.local.lonsdaleites.ru
- rabbitmq.sarex.local.lonsdaleites.ru
- minio.sarex.local.lonsdaleites.ru
issuerRef: issuerRef:
name: aero-ca-issuer # Боевой издатель, см. infrastructure/cert-manager/aero/configs.
name: letsencrypt-issuer-istio
kind: ClusterIssuer kind: ClusterIssuer

View File

@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: local-path-provisioner.yaml
target:
kind: HelmRelease
name: local-path-provisioner

View File

@ -0,0 +1,62 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: local-path-provisioner
namespace: local-path-provisioner
spec:
interval: 5m
timeout: 10m
values:
replicaCount: 1
image:
repository: cr.yandex/crp3ccidau046kdj8g9q/contour/local-path-provisioner-nn/local-path-provisioner
tag: v0.0.24
pullPolicy: IfNotPresent
helperImage:
repository: cr.yandex/crp3ccidau046kdj8g9q/contour/local-path-provisioner-nn/busybox
tag: latest
imagePullSecrets:
- name: regcred
defaultSettings:
registrySecret: null
privateRegistry:
registryUrl: null
registryUser: null
registryPasswd: null
storageClass:
create: true
# Класс по умолчанию: встроенный в k3s провижинер отключён
# (--disable=local-storage в docker-compose.yaml), и default-класса в
# кластере иначе не остаётся — PVC без явного storageClassName висели бы
# в Pending.
defaultClass: true
name: local-path
reclaimPolicy: Delete
# Путь ВНУТРИ контейнера ноды. Взят тот же, что использовал встроенный
# провижинер, потому что он уже смонтирован с хоста:
# ./k3s-storage/<нода> -> /var/lib/rancher/k3s/storage
# Благодаря этому данные PVC переживают пересоздание контейнеров нод и
# доступны для бэкапа обычными средствами хоста. Дефолт чарта
# (/opt/local-path-provisioner) никуда не смонтирован — данные исчезли бы
# вместе с контейнером ноды.
nodePathMap:
- node: DEFAULT_PATH_FOR_NON_LISTED_NODES
paths:
- /var/lib/rancher/k3s/storage
rbac:
create: true
serviceAccount:
create: true
name: ""
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
configmap:
name: local-path-config
setup: |-
set -eu
mkdir -m 0777 -p "$VOL_DIR"
teardown: |-
set -eu
rm -rf "$VOL_DIR"

View File

@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: minio.yaml
target:
kind: HelmRelease
name: minio

View File

@ -0,0 +1,40 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: minio
namespace: minio
spec:
interval: 5m
timeout: 10m
values:
nameOverride: "minio"
mode: standalone
replicas: 1
drivesPerNode: 1
imagePullSecrets:
- name: regcred
environment:
# Внешние адреса: консоль отдаётся с /console/ того же домена,
# маршрутизацию задаёт infrastructure/istio-config/aero.
MINIO_SERVER_URL: "https://minio.sarex.local.lonsdaleites.ru"
MINIO_BROWSER_REDIRECT_URL: "https://minio.sarex.local.lonsdaleites.ru/console/"
MINIO_API_CORS_ALLOW_ORIGIN: "https://minio.sarex.local.lonsdaleites.ru"
# Root-креды приезжают из Vault через vault-agent-injector: секрет кладёт
# ansible (роль sarex_stack, vault_app_secrets), роль minio в auth/kubernetes
# создаётся там же. В манифестах и в git паролей нет.
vaultRoot:
enabled: true
role: minio
authPath: auth/kubernetes
secretPath: secrets/data/minio/admin
rootUserKey: rootUser
rootPasswordKey: rootPassword
# В yc-k8s-test minio прибит к выделенным нодам (nodeSelector/tolerations
# dedicated=s3). В контуре aero таких нод нет — всего три, и планировщику
# ничего ограничивать не нужно.
persistence:
storageClass: local-path
size: 20Gi
resources:
requests:
memory: 512Mi

View File

@ -0,0 +1,14 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: rabbitmq.yaml
target:
kind: HelmRelease
name: rabbitmq
# JSON6902-патча с null здесь БОЛЬШЕ НЕТ — он не работал.
# kustomize нормализовал null обратно в {}, а пустую карту Helm сливает с
# дефолтами чарта, возвращая их целиком. Отключение перенесено в postRenderers
# внутри rabbitmq.yaml, где ресурсы вырезаются уже из готового манифеста.

View File

@ -0,0 +1,105 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: rabbitmq
namespace: rabbitmq
spec:
interval: 5m
timeout: 10m
# Вырезаем ресурсы ПОСЛЕ рендера чарта. Через values это недостижимо:
# * `null` до Helm не доезжает — kustomize нормализует его в {};
# * `{}` Helm сливает с дефолтом (coalesceMaps), и дефолт возвращается.
# postRenderers работает уже с готовым манифестом, где коалесинг позади.
#
# Что вырезаем: собственные Gateway/VirtualService/Certificate чарта на
# посторонний домен rabbitmq.infra.sarex.io. Маршрутизация контура описана
# централизованно в infrastructure/istio-config/aero.
#
# Особенно важен rmq-tls: домен не наш, ACME отдаёт 503 и проверка не пройдёт
# никогда. Пока он был в релизе, Helm ждал его выпуска, install падал по
# таймауту, срабатывал откат — и весь слой infra-controllers стоял.
postRenderers:
- kustomize:
patches:
- target:
kind: Gateway
name: rmq-gateway
patch: |
$patch: delete
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: rmq-gateway
namespace: gateway
- target:
kind: VirtualService
name: rmq-virt-service
patch: |
$patch: delete
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: rmq-virt-service
namespace: rabbitmq
- target:
kind: Certificate
name: rmq-tls
patch: |
$patch: delete
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: rmq-tls
namespace: gateway
values:
global:
security:
allowInsecureImages: true
# Собственные VirtualService/Gateway/Certificate чарта отключаются НЕ здесь,
# а JSON6902-патчем в kustomization.yaml рядом. Причина — в противоположной
# семантике null у двух инструментов:
#
# Helm : удалить дефолт можно ТОЛЬКО значением null;
# {} он глубоко сливает с дефолтом, и дефолт остаётся;
# kustomize : в strategic-merge-патче null означает УДАЛИТЬ КЛЮЧ,
# то есть до Helm он просто не доедет.
#
# Значит ни `virtualService: null`, ни `virtualService: {}` здесь не
# работают: первое стирается kustomize, второе игнорируется Helm. Нужен
# JSON6902, где null — это именно значение, а не команда удаления.
#
# Цена ошибки не косметическая: чарт создавал rmq-gateway, rmq-virt-service
# и сертификат rmq-tls на посторонний домен rabbitmq.infra.sarex.io.
# Последний не проходит проверку ACME никогда, впустую жжёт лимиты
# Let's Encrypt, а релиз висит в install, ожидая его выпуска, и блокирует
# весь слой infra-controllers.
metrics:
serviceMonitor:
enabled: false
default:
enabled: false
perObject:
enabled: false
detailed:
enabled: false
extraServiceMonitors: []
replicaCount: 1
persistence:
storageClass: local-path
size: 8Gi
resources:
requests:
memory: 512Mi
# Логин/пароль приезжают из Vault через vault-agent-injector: секрет кладёт
# ansible (роль sarex_stack, vault_app_secrets), роль rabbitmq в
# auth/kubernetes создаётся там же.
auth:
securePassword: true
existingPasswordSecret: ""
vault:
enabled: true
role: rabbitmq
authPath: auth/kubernetes
secretPath: secrets/data/rabbitmq/auth
usernameKey: username
passwordKey: password

View File

@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base

View File

@ -0,0 +1,49 @@
# Vault Agent Injector контура aero.
#
# Тот же чарт vault-contour, что и в остальных кластерах (это официальный
# hashicorp/vault-helm, перепубликованный в cr.yandex), но в режиме «внешний
# Vault»: сервер Vault здесь живёт в docker-compose, а в кластер ставится
# ТОЛЬКО injector.
#
# global.externalVaultAddr сам по себе отключает деплой vault-сервера — это
# документированное поведение чарта, отдельный server.enabled=false не нужен.
# Адрес указывает на DNS-мост (Service vault в namespace vault), который
# заводит ansible: k3s/manifests/vault/bridge-vault.yaml.
#
# Имя релиза — vault-injector, а не vault (как в прод-кластерах): чарт
# составляет имена ресурсов из имени релиза, и релиз `vault` рисковал бы
# перекрыть Service `vault` моста, оставив Vault недоступным.
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: vault-injector
namespace: vault
spec:
interval: 10m
chart:
spec:
chart: vault-contour
version: "0.2.3"
sourceRef:
kind: HelmRepository
name: yc-oci-charts
namespace: flux-system
interval: 10m
install:
createNamespace: true
remediation:
retries: 3
upgrade:
remediation:
retries: 3
values:
global:
# Отключает vault-сервер и задаёт адрес внешнего Vault для агентов.
externalVaultAddr: http://vault.vault.svc.cluster.local:8200
# Образы чарта лежат в приватном cr.yandex; секрет создаёт роль
# sarex_stack (registry_secrets), см. registry-secret.yaml.j2.
imagePullSecrets:
- name: regcred
tlsDisable: true
injector:
enabled: true

View File

@ -0,0 +1,11 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- helmrelease.yaml
# namespace vault здесь намеренно НЕ объявлен: его создаёт ansible вместе с
# DNS-мостом и token reviewer'ом (k3s/manifests/vault/), потому что Service
# моста нужно куда-то положить ещё до того, как Flux дойдёт до этого слоя.
# Дублировать объект в двух источниках правды — значит спорить за владение,
# поэтому чарту достаточно install.createNamespace на случай чистого кластера.

View File

@ -0,0 +1,41 @@
# DNS-мост: имя `vault` в namespace vault → compose-контейнер vault.
# Service без селектора + ручной Endpoints, по образцу bridge-gitea/bridge-postgres.
#
# Зачем: Vault контура живёт в docker-compose, а не в кластере (см. README).
# vault-agent-injector и внедрённые им в поды агенты работают ВНУТРИ k3s, где
# резолвит CoreDNS, и имя compose-сети `vault` там не существует. Поды ходят до
# 172.28.0.0/16 через SNAT на ноде, поэтому мост указывает на статический IP.
#
# __VAULT_BRIDGE_IP__ подставляет ansible (роль sarex_stack, tasks/vault-k8s.yml)
# из переменной VAULT_BRIDGE_IP — она же задаёт ipv4_address сервиса vault
# в docker-compose.yaml.
---
apiVersion: v1
kind: Namespace
metadata:
name: vault
---
apiVersion: v1
kind: Service
metadata:
name: vault
namespace: vault
spec:
ports:
- name: http
port: 8200
targetPort: 8200
protocol: TCP
---
apiVersion: v1
kind: Endpoints
metadata:
name: vault
namespace: vault
subsets:
- addresses:
- ip: __VAULT_BRIDGE_IP__
ports:
- name: http
port: 8200
protocol: TCP

View File

@ -0,0 +1,44 @@
# Token reviewer для метода auth/kubernetes.
#
# Vault живёт ВНЕ кластера, поэтому не может воспользоваться ни своим
# ServiceAccount-токеном, ни CA из /var/run/secrets. Чтобы он мог проверять
# токены подов, ему выдаётся отдельный ServiceAccount с правом обращаться к
# TokenReview API — ClusterRole `system:auth-delegator` (встроенная).
#
# Схема целиком: под с аннотациями vault.hashicorp.com/* → injector добавляет
# init-контейнер vault-agent → агент логинится в Vault токеном ServiceAccount'а
# пода → Vault валидирует этот токен через TokenReview от имени vault-auth →
# при успехе отдаёт секреты, агент кладёт их файлами в /vault/secrets/.
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-auth
namespace: vault
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-auth-delegator
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-auth
namespace: vault
---
# Долгоживущий токен ServiceAccount'а. С k8s 1.24 секрет-токен больше не
# создаётся автоматически при создании SA, а `kubectl create token` выдаёт
# токен с TTL — он протух бы, и Vault перестал бы валидировать логины.
# Секрет типа service-account-token с этой аннотацией заполняет
# token-controller: в него попадают и `token`, и `ca.crt` кластера.
apiVersion: v1
kind: Secret
metadata:
name: vault-auth-token
namespace: vault
annotations:
kubernetes.io/service-account.name: vault-auth
type: kubernetes.io/service-account-token