From c750a08f8d7ff6a6cef3c5d3181a9b6081a5f6bf Mon Sep 17 00:00:00 2001 From: emelinda Date: Sun, 9 Aug 2026 11:58:06 +0300 Subject: [PATCH] =?UTF-8?q?Measurements=20=D0=BF=D0=B8=D1=88=D0=B5=D1=82?= =?UTF-8?q?=20=D0=B2=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9=20=D0=B1=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BA=D0=BE=D0=BD=D1=82=D1=83=D1=80=D0=B0=20+?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=B0=D1=82=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BE=D0=BA=20=D0=B2=20ansible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Бакет --- В base у measurements имя бакета ЗАШИТО в самом vault-шаблоне: "buckets":["measurements"] Из-за этого он складывал файлы в собственный бакет, тогда как медиахранилище контура одно — sarex-media-storage: туда пишет django и оттуда же раздаёт s3-proxy на маршруте /media/. Ссылка на файл измерения, отданная через django, вела бы в пустоту. Шаблон переопределён в apps/measurements/aero так, чтобы имя бакета читалось из секрета — как уже читается endpoint. Значение задаётся в одном месте, vault_app_secrets, и совпадает с sarex_django_s3_bucket. Заменить пришлось аннотацию целиком: значение внутри шаблона, через values его не перекрыть. Из Job'а бакетов убран measurements. Это не удаление: Job умеет только mc mb --ignore-existing, ранее созданный бакет остаётся на месте — мы лишь перестаём его заводить. Порядок применения безопасен: в platform.yml vault-k8s.yml идёт до gitea-sync.yml, то есть секрет получит ключ bucket раньше, чем Flux пересоздаст под. Иначе vault-agent-init не отрендерил бы шаблон с несуществующим ключом и под ушёл бы в Init:Error. --- Перекат нагрузок --- Единственное действие, которое до сих пор делалось руками мимо роли, — rollout restart после правки ConfigMap. Оно понадобилось дважды (порядок set/rewrite в nginx и auth_type) и оба раза выглядело как «правка не поехала»: Flux рапортует Applied revision, а поведение старое. Причина в том, что у релизов universal-chart в шаблоне пода нет контрольной суммы конфига: ConfigMap меняется, спецификация пода — нет, Kubernetes не видит причин пересоздавать под. Декларативно это здесь не лечится — имя ConfigMap передаётся чарту строкой, монтирует он его сам, а configMapGenerator переименовал бы ресурс, не поправив ссылку внутри чарта, который рендерится уже в кластере. Добавлены tasks/reload.yml, список sarex_reload_workloads в defaults и задача `uv run poe reload`. По умолчанию выключено: перезапуск подов посреди обычного деплоя происходить сам не должен. --- aero/pyproject.toml | 5 +++ aero/roles/sarex_stack/defaults/main.yml | 30 ++++++++++++++-- aero/roles/sarex_stack/tasks/main.yml | 8 +++++ aero/roles/sarex_stack/tasks/reload.yml | 41 ++++++++++++++++++++++ apps/measurements/aero/kustomization.yaml | 25 ++++++++++--- infrastructure/minio/aero/buckets-job.yaml | 15 +++++--- 6 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 aero/roles/sarex_stack/tasks/reload.yml diff --git a/aero/pyproject.toml b/aero/pyproject.toml index 1c2cb1d..48819a4 100644 --- a/aero/pyproject.toml +++ b/aero/pyproject.toml @@ -47,6 +47,11 @@ login = "ansible-playbook deploy.yml --tags login" # Показать логин+пароль администратора платформы. Саму учётку создаёт под # backend при старте (apps/django/aero), эта задача только печатает креды. superuser = "ansible-playbook deploy.yml -e sarex_create_superuser=true --tags superuser" +# Перекатить нагрузки, конфигурация которых лежит в ConfigMap. Нужно после +# правки nginx-configmap: Flux обновит ConfigMap, но под продолжит работать со +# старым содержимым — в шаблоне пода нет хеша конфига, спецификация не меняется +# и Kubernetes не видит причин его пересоздавать. Список — sarex_reload_workloads. +reload = "ansible-playbook deploy.yml -e sarex_reload=true --tags reload" lint = "ansible-lint" # --- Операционные задачи на хосте (без полного деплоя) ---------------------- diff --git a/aero/roles/sarex_stack/defaults/main.yml b/aero/roles/sarex_stack/defaults/main.yml index b1dabb5..530d1fe 100644 --- a/aero/roles/sarex_stack/defaults/main.yml +++ b/aero/roles/sarex_stack/defaults/main.yml @@ -94,6 +94,28 @@ registry_secrets: sarex_compose_up: false compose_cmd: "docker compose" +# --- Перекат нагрузок после правки конфигурации ----------------------------- +# Выключено по умолчанию: перезапуск подов посреди обычного деплоя — не то, что +# должно происходить само. Включается задачей `uv run poe reload`. +# +# Список — это нагрузки, конфигурация которых лежит в ConfigMap и потому НЕ +# подхватывается без пересоздания пода: спецификация пода не меняется, значит +# Kubernetes не видит причин его перекатывать. Всё остальное перекатывается +# само — правка envs или podAnnotations меняет шаблон пода. +# +# Добавляешь ConfigMap, который читает приложение, — добавь нагрузку сюда, +# иначе правка будет уезжать в кластер и не применяться, а выглядеть это будет +# как «изменение не сработало». +sarex_reload: false +sarex_reload_timeout: 240s +sarex_reload_workloads: + # nginx главного фронтенда: apps/django/aero/nginx-configmap.yaml. Здесь + # маршруты на микрофронтенды workspaces и workflows и подмена + # /static/config.json, которой выключается Zitadel. + - namespace: django + deployment: frontend + why: nginx-configmap + # --- GitOps-подложка контура (k3s + gitea + vault + flux) ------------------- # Параметры gitea и Flux. Попадают в .env (их читает docker-compose) — менять # нужно здесь, а не в .env.example, иначе перезапишется при следующем деплое. @@ -309,8 +331,11 @@ vault_app_secrets: # собирает из этих ключей одну переменную S3_JSON_SETTINGS и читает endpoint # ВЛОЖЕННЫМ ключом .Data.data.client.endpoint — отсюда лишний уровень client. # - # Бакет measurements зашит в том же шаблоне и заводится Job'ом - # infrastructure/minio/aero/buckets-job.yaml. + # Бакет — ОБЩЕЕ медиахранилище контура, то же, куда пишет django и откуда + # раздаёт s3-proxy на маршруте /media/. В base у measurements имя бакета + # зашито в самом vault-шаблоне ("buckets":["measurements"]), из-за чего он + # складывал файлы отдельно и ссылка, отданная через django, вела бы в пустоту. + # Шаблон переопределён в apps/measurements/aero, чтобы имя читалось отсюда. # # Отдельного пользователя MinIO нет — тот же временный компромисс, что и у # django: берём root-креды. @@ -318,6 +343,7 @@ vault_app_secrets: data: client: endpoint: http://minio.minio.svc.cluster.local:9000 + bucket: "{{ sarex_django_s3_bucket }}" access_key: "{{ sarex_minio_k8s_user }}" secret_key: "{{ sarex_secrets.SAREX_MINIO_ROOT_PASSWORD }}" # Учётка администратора платформы. Её разбирает не приложение, а стартовый diff --git a/aero/roles/sarex_stack/tasks/main.yml b/aero/roles/sarex_stack/tasks/main.yml index 2772494..e63683a 100644 --- a/aero/roles/sarex_stack/tasks/main.yml +++ b/aero/roles/sarex_stack/tasks/main.yml @@ -194,6 +194,14 @@ when: aero_platform_up | bool tags: [platform] +# --- Перекат нагрузок с изменённой конфигурацией -------------------------- +# Только по тегу reload: правка ConfigMap сама под не перезапускает, а делать +# это на каждом прогоне нельзя. Обоснование — в шапке reload.yml. +- name: Перекатить нагрузки с изменённой конфигурацией + ansible.builtin.include_tasks: reload.yml + when: sarex_reload | bool + tags: [reload] + # --- Поднять сервисы приложения ------------------------------------------ - name: Поднять сервисы приложения (docker compose up -d) ansible.builtin.command: diff --git a/aero/roles/sarex_stack/tasks/reload.yml b/aero/roles/sarex_stack/tasks/reload.yml new file mode 100644 index 0000000..6d3d8e3 --- /dev/null +++ b/aero/roles/sarex_stack/tasks/reload.yml @@ -0,0 +1,41 @@ +--- +# Перекат нагрузок, которые не перезапускаются сами при правке конфигурации. +# +# ЗАЧЕМ ЭТО ОТДЕЛЬНАЯ ЗАДАЧА. Flux обновляет ConfigMap в кластере за пару минут, +# но под продолжает работать со старым содержимым: у релизов universal-chart в +# шаблоне пода нет контрольной суммы конфига, поэтому спецификация не меняется +# и Kubernetes не видит причин пересоздавать под. Всё, что задаётся через +# ConfigMap, применяется только после явного переката. +# +# Так это выглядит на практике: правишь nginx.conf, Flux рапортует Applied +# revision, проверяешь — поведение старое. Легко принять за «правка не поехала» +# и пойти искать несуществующую ошибку. Дважды так и было: сначала с порядком +# set/rewrite, потом с auth_type. +# +# ПОЧЕМУ НЕ ДЕКЛАРАТИВНО. Правильное лечение — аннотация с хешем конфига в +# шаблоне пода, тогда изменение ConfigMap меняет спецификацию и перекат +# происходит сам. Здесь так не выходит: имя ConfigMap чарту передаётся строкой, +# монтирование он делает сам, и посчитать хеш на этапе kustomize нечем — +# configMapGenerator переименовал бы ресурс, но ссылку внутри чарта, который +# рендерится уже в кластере, не поправил бы. Так что действие остаётся +# императивным, и его место — здесь, а не в чьей-то памяти. +# +# ЗАПУСК ТОЛЬКО ЯВНЫЙ. Задача не выполняется при обычном прогоне: перекат +# нагрузок посреди деплоя — не то, что должно случаться само. +# uv run poe reload +# либо точечно: +# uv run poe reload -- -e "sarex_reload_workloads=[{'namespace':'django','deployment':'frontend'}]" + +- name: Перекатить нагрузки с изменённой конфигурацией + ansible.builtin.shell: + cmd: >- + {{ compose_cmd }} exec -T k3s-server sh -ec ' + kubectl -n {{ item.namespace }} rollout restart deploy/{{ item.deployment }} + kubectl -n {{ item.namespace }} rollout status deploy/{{ item.deployment }} --timeout={{ sarex_reload_timeout }} + ' + chdir: "{{ deploy_dir }}" + loop: "{{ sarex_reload_workloads }}" + loop_control: + label: "{{ item.namespace }}/{{ item.deployment }} — {{ item.why | default('правка конфигурации') }}" + changed_when: true + tags: [reload] diff --git a/apps/measurements/aero/kustomization.yaml b/apps/measurements/aero/kustomization.yaml index a666f7e..f43260d 100644 --- a/apps/measurements/aero/kustomization.yaml +++ b/apps/measurements/aero/kustomization.yaml @@ -19,8 +19,23 @@ patches: path: /spec/values/services/backend/service/name/_default value: measurements-service - # S3 внутри кластера. В base endpoint приезжает из самого секрета - # (.Data.data.client.endpoint), поэтому шаблон править не нужно — адрес - # задаёт ansible в vault_app_secrets. Бакет measurements заводит - # infrastructure/minio/aero/buckets-job.yaml: имя зашито в шаблоне - # аннотации, и без готового бакета приложение не поднимется. + # S3: бакет берём из секрета, а не из шаблона. + # + # В base имя бакета ЗАШИТО прямо в vault-шаблоне — "buckets":["measurements"]. + # Из-за этого measurements складывал файлы в собственный бакет, тогда как + # медиахранилище контура одно и называется sarex-media-storage: туда пишет + # django (S3_BUCKET) и оттуда же раздаёт s3-proxy на маршруте /media/. + # Разные бакеты означали, что ссылка на файл измерения, отданная через + # django, вела бы в пустоту. + # + # Endpoint в base уже приезжает из секрета (.Data.data.client.endpoint) — + # достраиваем до той же схемы и бакет, чтобы имя задавалось в ОДНОМ месте + # (vault_app_secrets в aero/roles/sarex_stack/defaults/main.yml), а не + # дублировалось в манифесте. Заменить можно только аннотацию целиком: + # значение зашито внутри шаблона, через values его не перекрыть. + - op: replace + path: /spec/values/services/backend/podAnnotations/_default/vault.hashicorp.com~1agent-inject-template-measurements-s3 + value: |- + {{- with secret "secrets/data/minio/apps/measurements" -}} + S3_JSON_SETTINGS='{"host":"{{ index .Data.data.client "endpoint" }}","login":"{{ index .Data.data "access_key" }}","password":"{{ index .Data.data "secret_key" }}","verify":false,"buckets":["{{ index .Data.data "bucket" }}"]}' + {{- end -}} diff --git a/infrastructure/minio/aero/buckets-job.yaml b/infrastructure/minio/aero/buckets-job.yaml index 1950af8..6ef22be 100644 --- a/infrastructure/minio/aero/buckets-job.yaml +++ b/infrastructure/minio/aero/buckets-job.yaml @@ -1,8 +1,7 @@ --- # Заведение бакетов. Чарт MinIO этого не умеет — форк не принимает ни buckets, -# ни provisioning, — а приложения ждут бакеты готовыми: django падает на -# загрузке файлов, measurements — на старте, потому что имя бакета зашито в -# его vault-шаблоне. +# ни provisioning, — а приложения ждут бакет готовым: django падает на загрузке +# файлов, measurements — на старте, если бакета из его vault-шаблона нет. # # До этого Job'а бакеты заводились руками через mc. Такой бакет не переживает # пересоздание контура и не виден в репозитории: развернув всё с нуля, ты @@ -66,7 +65,15 @@ spec: sleep 5 done - for b in sarex-media-storage measurements; do + # Бакет в контуре ОДИН: в него пишет django, из него раздаёт + # s3-proxy на маршруте /media/, туда же переведён measurements. + # Раньше у measurements в vault-шаблоне был зашит собственный + # бакет, и файл, положенный им, по ссылке из django не отдавался. + # + # Ранее созданный бакет measurements этой правкой НЕ удаляется: + # Job умеет только mc mb --ignore-existing. Убирать его — отдельное + # осознанное решение, здесь мы лишь перестаём его заводить. + for b in sarex-media-storage; do mc mb --ignore-existing "contour/$b" echo "бакет $b готов" done