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