diff --git a/.wiki/sources/.gitkeep b/.wiki/sources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.wiki/sources/iis-host-migration-2026-05-19.md b/.wiki/sources/iis-host-migration-2026-05-19.md new file mode 100644 index 0000000..1396631 --- /dev/null +++ b/.wiki/sources/iis-host-migration-2026-05-19.md @@ -0,0 +1,275 @@ +--- +title: IIS Host Migration Session 2026-05-19 +type: source +tags: [migration, iis, windows, recovery, traefik] +ingested: 2026-05-19 +raw_path: ../../.tasks/iis-on-host-migration.md +updated: 2026-05-21 +--- + +# IIS Host Migration Session 2026-05-19 + +Перенос основного CMS-сайта (`MoreThenCms.Web`, обслуживает 11 клиентских доменов через Host header) с IIS внутри [[snolla-recovery-vm]] на нативный IIS [[windows-recovery-host]]. Цель — убрать VBox как слой нестабильности. + +## Хронология + +**Старт сессии** (после [[nas-recovery-session-2026-05-18]]) — VM один раз отвалилась сетью, оживили через `VBoxManage guestcontrol` + `ipconfig /release /renew` (recipe в [[vbox-windows-stability-tuning]] подтверждён ещё раз). + +**Phase 1 — discovery (read-only):** +- В VM через SSH (NAT-forward 127.0.0.1:8022, не bridged 192.168.1.15 — это устарело): `appcmd list site/apppool/app/vdir /xml`. +- На хосте: `Get-WindowsOptionalFeature -Online IIS-*` через elevated PS — все нужные фичи (`IIS-WebServer`, `IIS-ASPNET45`, `IIS-NetFxExtensibility45`, `IIS-ISAPIFilter`, `IIS-ManagementConsole`, `IIS-IIS6ManagementCompatibility`, `IIS-Metabase`) уже установлены. `WebAdministration` PS-module грузится из admin-PS. +- В источниках на хосте (`grep`): hardcoded `C:\inetpub\wwwroot\` в коде CMS нет — всё через `ConfigurationManager.AppSettings["sitePath"]`. **НО** в prod Web.config (из backup `vm-sites\`) ключ `sitePath` явно проставлен абсолютным путём — патч в Phase 2 необходим. + +**Phase 1 находки (важные расхождения с wiki):** +- `Snolla.IdentityManager` в VM **на :8089, не :80** (вики говорила :80). Sub-apps stostayer.old `/calc`, `/price`, `/price/tireService` существуют с отдельными app pools. +- `C:\stayer\Snolla.IdentityManager\Web.config` указывает на **`SRV-1135520-1\SQLEXPRESS` + Integrated Security** — мёртвый внешний сервер. Сайт точно не работает, deploy-артефакт без живого консьюмера. +- `C:\stayer\MoreThenCms.Web\Web.config` (stostayer) использует **внешний production MSSQL `89.253.219.2,1433`** с user `stostayer` — это **другая инфраструктура**, не наш контейнер. + +**Phase 2 — миграция (admin PS):** +- `Default Web Site` остановлен (`autoStart=false`). +- `robocopy` из `C:\nas-recovery\vm-sites\` → `C:\sites\MoreThenCms.Web` (8.7 GB / 44.7k файлов / 1m13s) и → `C:\stayer\` (2.2 GB / 13.9k файлов / 21s). +- Web.config patch только для `C:\sites\MoreThenCms.Web\Web.config`: `sitePath` `C:\inetpub\wwwroot\MoreThenCms.Web\` → `C:\sites\MoreThenCms.Web\` + `Data Source=10.0.2.2` → `Data Source=localhost`. UTF-8 with BOM через `[System.IO.File]::WriteAllText` (паттерн из [[cms-config-rewrite-pattern]]). +- 3 AppPool (`.NET v4.0` Integrated, `ApplicationPoolIdentity`): `MoreThenCms.Web`, `stostayer`, `stostayer.old`. +- 3 IIS-сайта: `MoreThenCms.Web *:80` (catch-all), `stostayer *:8090`, `stostayer.old *:8091`. +- ACL `icacls /grant 'IIS AppPool\:(OI)(CI)M' /T` на каждый physical root. +- Skip: `Snolla.IdentityManager` (по решению — публично не нужен), 3 sub-apps stostayer.old (по решению). +- Smoke test через `http://localhost/` с Host header: все 11 хостов отвечают (большинство 302 redirect — CMS работает). `rimiz.ru` — 404 (CMS-side, не инфра). stostayer/oldstostayer на :8090/:8091 — timeout, **в текущей сессии не разбирались**. + +**Phase 3 — traefik switch (file-provider auto-reload):** +- Backup `*.yml.bak-phase3-2026-05-19` для 11 файлов в `C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\`. +- Patch: `host.docker.internal:18080` → `host.docker.internal:80` (11 yml: snolla, rimiz, labtools, labtoolspro, pilorama98, tandemmebel, emspb, kupimknigi, maljarka, sestech, isc-artmaterials). UTF-8 без BOM. +- **Не трогали** `stostayer.yml` (`:18180`) и `oldstostayer.yml` (`:18181`) — stayer-сайты остаются на VM. +- Public smoke через `https://localhost:4443/` с Host header: **10/11 → HTTP 200**. Только `rimiz.ru` → 404 (как и в локальном тесте). + +## Результат + +Production-трафик 10 главных доменов теперь идёт **полностью без VM**: client → OpenWRT → traefik → host-IIS native → MSSQL container на хосте. VM продолжает обслуживать только stostayer/oldstostayer через старые NAT port forwards (`:18180/:18181`). + +## Открытые вопросы / нюансы + +- **`rimiz.ru` отдаёт 404** на host-IIS (и на VM до миграции тоже отдавал?). CMS-routing не знает этот host. Проверить mapping в БД / CMS-сайт-таблице. +- **stostayer + stostayer.old timeout** на host-IIS (`http://localhost:8090/`, `:8091`). Возможно cold-start ASP.NET, возможно реальный bug. Сейчас не разбирали — traefik для них всё ещё на VM. +- **Snolla.IdentityManager не мигрирован** — если что-то внутри CMS дёргает его через `localhost:8089`, может тихо ломаться (пока симптомов не видно). +- **VM можно глушить** только после: либо доделать stostayer на host, либо явное решение не мигрировать stayer на этот хост. +- **Phase 5 не сделан**: URL Rewrite + ARR (фикс `:4443` в редиректах CMS), Application Initialization (warm-start), log rotation `C:\inetpub\logs\`. + +## Технические артефакты + +- `.tasks/iis-on-host-migration.md` — детальный план фаз, обновлён со статусом Phase 1-3 = done. +- Backup конфигов traefik: `*.yml.bak-phase3-2026-05-19`. +- Скрипты Phase 2 и Phase 3 — в истории чата сессии (не сохранены отдельным файлом, идемпотентны на повторный запуск). + +## Phase 4 — реорг `C:\sites\` + перенос stayer'ов (вечер 2026-05-19) + +Пользователь возмутился разбросом (`C:\sites\` только для snolla, `C:\stayer\` для stayer). Реорганизация ради единой иерархии: + +- `C:\sites\MoreThenCms.Web` → `C:\sites\snolla` (rename) +- `C:\stayer\MoreThenCms.Web` → `C:\sites\stostayer` (move) +- `C:\stayer\stostayer.old` → `C:\sites\stostayer.old` (move) +- `C:\stayer\Snolla.IdentityManager` → `C:\sites\snolla-identity-manager` (move + kebab-rename) +- Удалены: `C:\stayer\Mis.StoStayer.{Price,TireService,Calculator}.{Api,Web}` (sub-apps не нужны). +- `C:\stayer\` полностью удалён. + +IIS sync: +- Site `MoreThenCms.Web` → переименован в `snolla` (`Set-ItemProperty IIS:\Sites\... -Name name`) +- AppPool `MoreThenCms.Web` — **rename невозможен in-place**, пересоздан как `snolla` с тем же набором properties (.NET v4.0 Integrated, `ApplicationPoolIdentity`), site rebound, старый pool удалён. +- `physicalPath` всех 3 sites обновлён под `C:\sites\`. +- Web.config sitePath patches: + - `C:\sites\snolla\Web.config`: `C:\sites\MoreThenCms.Web\` → `C:\sites\snolla\` + - `C:\sites\stostayer\Web.config`: `C:\stayer\MoreThenCms.Web\` → `C:\sites\stostayer\` +- ACL re-grant + cleanup stale ACE для `IIS AppPool\MoreThenCms.Web`. + +## Phase 5 — stostayer.old DB-conn патч (запутанная история conn-string'ов в stayer\) + +Локальный smoke `:8091` падал в timeout. Причина: `C:\sites\stostayer.old\web.config` имел `Data Source=10.0.2.2` (адрес VBox NAT gateway, на хосте не резолвится) с user=`snolla` — фактически идентичная нашему MSSQL контейнеру конфигурация, только адрес неправильный. Patch: `10.0.2.2` → `localhost`. После — `:8091` → 200. + +Это **наша ошибка** Phase 2 — пропустили patch этого файла, так как фокус был только на `MoreThenCms.Web\Web.config`. + +## Phase 6 — stostayer DB-conn миграция на новый сервер (`www.stostayer.ru`) + +Локальный smoke `:8090` тоже timeout — но по другой причине. `C:\sites\stostayer\Web.config` указывал на `89.253.219.2,1433` (внешний production MSSQL), оказался **полностью недоступен**: TCP timeout, ping fail, traceroute затухает на 8-м hop у `139.45.230.171`. На VM (через тот же путь) тоже не работал — пользователь подтвердил. + +Пользователь предоставил новые creds: `www.stostayer.ru,1433` / `stayer_site` / `^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF`. Тест из не-elevated PS: TCP reachable, SQL login OK (SQL Server 2022). Patched Web.config. + +**Pitfall:** после patch IIS отдавал HTTP 500. Причина — символ `&` в пароле, который **в XML является зарезервированным**. ASP.NET Web.config XML парсер падал на парсинг conn-string. Fix: `&` → `&` (XML entity escape). См. [[webconfig-password-xml-escape]]. + +## Phase 7 — отключение traefik routes для stayer'ов + +Пользователь решил: stayer'ы наружу светить не нужно вообще (даже после миграции на хост). `stostayer.yml` и `oldstostayer.yml` переименованы в `*.yml.disabled` — traefik file-provider не подхватывает, route gone. Site'ы на хосте `:8090/:8091` остаются для возможного внутреннего использования. + +Backup yml для этих изменений: `*.yml.bak-stayer-switch-2026-05-19` (содержит вариант с переключением на `:8090/:8091` — на случай если решим включить обратно). + +## Phase 8 — VM savestate + +После того как 100% prod-трафика идёт через host-IIS, VM `snolla-recovery` заморожена через `VBoxManage controlvm "snolla-recovery" savestate`. VMState = `saved`. Конфигурация и диски сохранены — resume за 5-10 сек если что-то понадобится. Не удалена. + +## Финальное состояние (2026-05-19 конец сессии) + +``` +C:\sites\ + ├── snolla\ (was MoreThenCms.Web in C:\inetpub\wwwroot, was in vm-sites\wwwroot\MoreThenCms.Web) + ├── stostayer\ (was C:\stayer\MoreThenCms.Web) + ├── stostayer.old\ (was C:\stayer\stostayer.old) + └── snolla-identity-manager\ (was C:\stayer\Snolla.IdentityManager — deploy artifact, без живого consumer'а) + +IIS sites/pools (имя=имя=пуло): + snolla *:80 → C:\sites\snolla (pool snolla, .NET v4.0 Integrated, AppPoolIdentity) + stostayer *:8090 → C:\sites\stostayer (pool stostayer) + stostayer.old *:8091 → C:\sites\stostayer.old (pool stostayer.old) + +Connection strings: + snolla: Data Source=localhost → MSSQL container на хосте (DB MoreThenCms) + stostayer: Data Source=www.stostayer.ru,1433 (user stayer_site) → внешний SQL Server 2022 + stostayer.old: Data Source=localhost → MSSQL container на хосте (DB stostayer, user snolla) + +Traefik active routes: 10 главных доменов (snolla.com и др.) + rimiz.ru → host:80. + Stayer routes (stostayer.snolla.com, old.stostayer.ru) DISABLED — приватны. + Mёртвые: disk.yml, dsm.yml (192.168.1.10). + +VM snolla-recovery: VMState=saved (frozen, ~92 GB на диске + ~3 GB savestate RAM dump). +``` + +## Открытые вопросы (что унесли в следующую сессию) + +- **`rimiz.ru` отдаёт 404** на host-IIS. CMS-routing не знает этот host — нужно копнуть БД (таблица CMS-сайтов, mapping host header → site). +- **Snolla.IdentityManager** на хосте: папка перенесена, но IIS-сайт не создавался (по решению). Если CMS-код где-то дёргает `localhost:8089` или подобное — будет тихий fail. Симптомов пока нет. +- **Phase 5 (из старого плана)**: URL Rewrite + ARR для фикса `:4443` в редиректах, Application Initialization (warm-start), log rotation `C:\inetpub\logs\`. Не блокеры. +- **VM-cleanup**: если через ~неделю стабильной работы host'а проблем не будет — `VBoxManage unregistervm "snolla-recovery" --delete` освободит ~92 GB. NAT port forwards в OpenWRT (host:18080/18180/18181/18189) можно удалить. + +## Связано + +[[recovery-architecture-snapshot]] обновлён под полностью-host chain. [[snolla-recovery-vm]] — VMState=saved, все sites unused. [[windows-recovery-host]] — финальный layout `C:\sites\`. [[webconfig-password-xml-escape]] — новый concept про XML entity escape в conn-string'ах. [[cms-config-rewrite-pattern]] — UTF-8 BOM подтверждён ещё много раз. + +--- + +## Phase 9 — RЕVERT (вечер 2026-05-19, после провала) + +**Что случилось:** через ~10 минут после моего commit'a "done" пользователь открыл `https://www.pilorama98.ru/` → `502 Bad Gateway`. Дальше остальные домены показали `TOO_MANY_REDIRECTS`. + +**Root cause** (расписан полностью в [[iis-migration-2026-05-19-postmortem]]): +- Phase 3 я patched traefik backend `host.docker.internal:18080` → `host.docker.internal:80`. +- Внутри traefik-контейнера `host.docker.internal:80` через Docker Desktop NAT резолвится **обратно в сам traefik** на его HTTP entrypoint :80 (potential Docker publish-port loopback gotcha). +- traefik `https.yml` имеет http-catchall middleware `redirect-to-https` → traefik отвечает 301 на свой же запрос → loop. + +**Реактивная цепочка ошибок** (~1 час): +- Restart-WebAppPool, nuke workers, docker restart traefik → only sделали хуже (503). +- Patched `host.docker.internal:80 → 192.168.1.143:80` → тот же loop (LAN-IP через NAT тоже возвращается в traefik). +- Patched на `:8088` + добавил IIS binding → нужен `Stop+Start Website` чтобы binding applied, изначально забыл → IIS не listen → 503. +- Думал что 301 от CMS (Pitfall 5 X-Forwarded-Proto), копал CMS код — реальный источник был самим traefik (headers без `Server: Microsoft-IIS` подсказывали). + +**Revert (выполнен):** +- `VBoxManage startvm "snolla-recovery"` — VM поднята из savestate (~10s + 90s warmup + recipe `ipconfig /release /renew` для застрявшего network adapter из [[vbox-windows-stability-tuning]]). +- 11 главных yml восстановлены из `*.bak-phase3-2026-05-19` (`host.docker.internal:18080` → VM). +- Stayer yml: `stostayer.yml.disabled / oldstostayer.yml.disabled` удалены, восстановлены из `*.bak-stayer-switch-2026-05-19` (`:18180/18181` → VM). +- `docker restart traefik` (file-provider не подхватил reload автоматом). +- Public smoke через VM-chain → 7/11 хостов отвечают (2 c 200, 5 с CMS-side редиректами на canonical — нормально для CMS-логики); проверено пользователем в браузере → работает. + +## Финальное состояние (после revert) + +Production снова на VM-chain (как было в начале сессии 2026-05-19). На хосте осталось: +- `C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager}` — ~11 GB, **inert** (не на prod-пути) +- IIS sites/pools `snolla, stostayer, stostayer.old` — не получают трафика (traefik backend назад на VM) +- traefik backups: `*.bak-phase3-2026-05-19`, `*.bak-stayer-switch-2026-05-19`, `*.bak-hostip-2026-05-19` +- VM `snolla-recovery` — running, IIS активен, обслуживает 11 главных доменов + 2 stayer публично через port forwards. + +**Артефакты для следующей попытки** (НЕ удалять): +- Web.config'и в `C:\sites\` пропатчены правильно (sitePath, conn-strings, stostayer на новый DB) — можно переиспользовать. +- IIS sites/pools уже настроены — переключение требует только traefik backend patch + tested correctly. +- Recipe для правильной миграции — в [[iis-migration-2026-05-19-postmortem]]. + +--- + +## Phase 10 — Attempt 2 (вечер 2026-05-19, после прочтения post-mortem) + +Повторная попытка миграции, выполненная **по recipe из [[iis-migration-2026-05-19-postmortem]]**. Все 5 главных правил соблюдены, миграция прошла без incidents. + +### Что сделано + +1. **Read-only sanity (без prod-touches):** VM running confirmed, port-scan на хосте показал `:18090` и `:8089` свободны, traefik dir contains 4 серии bak-stamp'ов сохранённых от attempt 1, IIS sites/pools (`snolla, stostayer, stostayer.old`) живы. +2. **Backend port: `:8089`** (recipe-A — НЕ `:80`, НЕ совпадает с traefik publish `:8000/:4443/:8080`, НЕ совпадает с VM NAT forwards `:18080/:18180/:18181`). Выбор user'а (был кандидат `:18090`, user предпочёл `:8089`). +3. **Atomic revert plan ДО старта** (recipe-E): новая bak-серия `.bak-pre-attempt2-2026-05-19` для всех 13 yml (11 cms + 2 stayer), paste-ready команда восстановления записана в `.tasks/STATUS.md`. +4. **IIS binding** `snolla *:8089` добавлен через elevated PS (`New-WebBinding` + `Stop-Website; Start-Website` — recipe-A note: без restart binding не активируется). +5. **Loop-detect через `docker exec`** (recipe-A + recipe-F): `docker exec traefik wget --spider -S --header="Host: emspb.ru" http://host.docker.internal:8089/` → first response `301 → http://www.emspb.ru/` от `Server: Microsoft-IIS/10.0`. `host.docker.internal` resolves to `192.168.65.254:8089` — это Docker Desktop host gateway, **не traefik publish-port**. NO NAT loop. См. [[docker-host-loopback-detect]] про общую технику. +6. **Smoke test с `-MaximumRedirection 0`** (recipe-B): `curl.exe -k -I --max-redirs 0 -H "Host: emspb.ru" https://localhost:4443/` → first response `301`, `Server: Microsoft-IIS/10.0`, `Location: http://www.emspb.ru/`. Это **expected CMS canonical redirect** (та же логика что была на VM). +7. **Canary atomic** — patched ОДИН yml (`emspb.yml`), file-provider auto-reload, public smoke clean → 📱 **phone-test с мобильного интернета** (recipe-C) → `https://emspb.ru/` открылось → ✅. +8. **Second canary** — `labtools.yml`, smoke + phone-test → ✅. +9. **Batch patch** оставшихся 9 cms yml (snolla, rimiz, labtoolspro, pilorama98, tandemmebel, kupimknigi, maljarka, sestech, isc-artmaterials) одним PS-блоком (recipe-G: batch не item-by-item) → smoke 20 hostnames → все `Server: Microsoft-IIS/10.0` ✅. +10. **Stayer routes — disabled** (re-confirmed Phase 7 decision): user подтвердил «stayer'ы локальные, через traefik наружу не светят». Rename `stostayer.yml → stostayer.yml.disabled`, `oldstostayer.yml → oldstostayer.yml.disabled`. `docker restart traefik` (file-provider не подхватил deletion auto-reload). Verify: `stostayer.snolla.com`/`old.stostayer.ru` → `404 text/plain` (traefik no-route). Host IIS sites `:8090`/`:8091` остаются live для прямого/локального доступа. +11. **VM running parallel** (recipe-D): VM **НЕ savestate'ить** минимум 24h+. Освободит RAM/disk только после подтверждённой стабильности host'а. + +### Pitfall found and resolved + +- **WinHTTP proxy strips Server header.** Локальный `curl.exe http://localhost:8090/` returned response без `Server: Microsoft-IIS/10.0` и без `X-Powered-By: ASP.NET` (плюс с `Proxy-Connection: keep-alive`) — выглядело как «не IIS отвечает». Это привело к moment'у panic. Но `docker exec traefik wget ...` (real production path) **показал headers корректно**. Вывод: для loop-detect / IIS-confirmation тестов **использовать `docker exec` из traefik container**, не Windows `curl.exe` — последний ходит через Windows-уровневый proxy который headers вырезает. + +### My mistake during this session + +- Я неверно интерпретировал «мигрировать stayer'ов» как «patch traefik backend → host:8090/8091» (т.е. пускать через traefik наружу). User имел в виду «host IIS уже на :8090/:8091, traefik routes должны быть **DISABLED**». Сделал prod-changing patch → user интервент-stop → revert + rename `.yml.disabled`. См. memory `feedback-migrate-semantics`. Lesson — переспрашивать semantics для internal/low-traffic сервисов перед prod-changing. + +### Финальный chain (после attempt 2) + +``` +Клиент (browser) + → DNS → 94.19.247.14 (public IP) + → OpenWRT NAT 443 → 192.168.1.143:4443 + → traefik:4443 (TLS termination) + → match Host → одно из 11 cms-yml → backend + → http://host.docker.internal:8089/ + ↳ Docker Desktop host gateway 192.168.65.254:8089 + → Windows host IIS site `snolla` (*:80 + *:8089 bindings) + → C:\sites\snolla\, .NET Framework 4.8.1, ApplicationPoolIdentity + → conn → MSSQL container на host:1433 + ← HTTP response +``` + +Stayer chain: traefik routes disabled. `stostayer.snolla.com` / `old.stostayer.ru` → traefik 404. Host IIS sites `:8090`/`:8091` живут для локального доступа. + +VM `snolla-recovery`: **running parallel** (24h+ soak), backend в traefik больше не используется — VM NAT port forwards (`:18080/:18180/:18181`) холостые. + +### Артефакты этой сессии + +- `.tasks/STATUS.md` — статус task'а 🔴 active. +- traefik backups в `data/custom/`: `*.yml.bak-pre-attempt2-2026-05-19` для 13 yml, плюс `stostayer.yml.disabled`, `oldstostayer.yml.disabled`. +- `feedback-migrate-semantics` memory — урок для будущих сессий. +- `concepts/docker-host-loopback-detect.md` — новый concept для loop-detect technique. + +## Что осталось open + +- **24h+ soak** до savestate VM (минимум до утра 2026-05-20, лучше 48h до 2026-05-21). +- **VM cleanup**: после soak — `savestate` (освободит RAM), позже `unregistervm --delete` (освободит ~92 GB). +- **Port forwards OpenWRT** (`host:18080/18180/18181/18189` → VM) — больше не нужны, удалить позже. +- **rimiz.ru / ics-artmaterials.com 404** — CMS-side routing issues, не инфра. Открытым. +- **X-Forwarded headers** (`:4443` в redirect URL) — известный bug [[traefik-on-windows-docker-desktop]] Pitfall 5, отдельная задача. + +## Phase 11 — close-out 2026-05-21 (~36h soak) + +Soak window 2026-05-19 22:00 MSK → 2026-05-21 ~08:00 MSK ≈ 36 hours. Verify: + +- **Traefik:** `Up 38 hours` (`docker ps`) — zero restarts, zero unscheduled bouncing. +- **w3wp.exe** (IIS worker для `snolla` site): PID 10432, CreationDate 2026-05-20 23:19:21 MSK, uptime 8.8h. Это **scheduled IIS app pool recycle** (default 1740 min ≈ 29h), не crash — нормальное поведение pool'а после ~29h работы. Память 522 MB. +- **HTTPS smoke** 11 заявленных hosts через `docker exec traefik wget --spider --server-response https:///`: + +| Host | Code | Server | Verdict | +|---|---|---|---| +| `emspb.ru` | 301→www | Microsoft-IIS/10.0 | ✅ | +| `snolla.com` | 301→on.snolla | Microsoft-IIS/10.0 | ✅ | +| `on.snolla.com` | 200 | Microsoft-IIS/10.0 | ✅ | +| `pilorama98.ru` | 301→www | Microsoft-IIS/10.0 | ✅ | +| `labtools.ru` | 301→www | Microsoft-IIS/10.0 | ✅ | +| `labtools.pro` | 301→www | Microsoft-IIS/10.0 | ✅ | +| `tandemmebel.ru` | 301→www | Microsoft-IIS/10.0 | ✅ | +| `kupimknigi.spb.ru` | 200 | Microsoft-IIS/10.0 | ✅ | +| `maljarka.ru` | — | DNS bad address | ⚠️ off-infra (nslookup 8.8.8.8 → no A record) | +| `sestech.ru` | — | TLS cert `CN=*.domainparking.ru` (expired 2026-05-13) | ⚠️ off-infra (domain parking provider) | +| `ics-artmaterials.com` | 200 | `nginx-reuseport/1.21.1` | ⚠️ off-infra (A→87.236.16.28, мигрирован на сторонний WP-хостинг) | + +**Итог:** 8/8 наших sites зеленые. 3 hostname'а из исходного списка task'а оказались **уже не на нашей инфраструктуре** — DNS либо снят, либо переключен на сторонние хостинги. Это не regression миграции; эти domains надо просто убрать из traefik/IIS routing'а как dead. + +Decisions: +- iis-host migration close-out: **DONE 2026-05-21**. +- VM `snolla-recovery`: готова к savestate (soak passed, zero rollbacks). User-side savestate решает отдельно. +- 3 off-infra domains: новая задача — `iis-traefik-dead-routes-cleanup` (low prio). + +## Артефакты Phase 11 + +- `.tasks/STATUS.md` — task → 🟢 done. +- (existing) traefik bak-серия `.bak-pre-attempt2-2026-05-19` — оставить ещё неделю (atomic revert на случай unforeseen regression). diff --git a/.wiki/sources/nas-recovery-session-2026-05-18.md b/.wiki/sources/nas-recovery-session-2026-05-18.md new file mode 100644 index 0000000..bfc2e96 --- /dev/null +++ b/.wiki/sources/nas-recovery-session-2026-05-18.md @@ -0,0 +1,78 @@ +--- +title: NAS Recovery Session 2026-05-18/19 +type: source +tags: [recovery, nas, synology, virtualbox, traefik, mssql, minio, elasticsearch] +ingested: 2026-05-19 +raw_path: ../../.tasks/nas-recovery.md +updated: 2026-05-19 +--- + +# NAS Recovery Session 2026-05-18/19 + +15-часовая сессия восстановления клиентских сайтов после краха NAS Synology, на котором они хостились. Источник истины — лог переписки восстановления + `.tasks/nas-recovery.md`. Здесь сжатая хронология; конкретные паттерны и решения распилены по `concepts/`, инфраструктурные сущности — по `entities/`. + +## Контекст до краха + +- **Source NAS (теперь мёртвый):** Synology DiskStation на XPEnology (самосборное x86 железо + DSM через community-loader). На нём: + - VMM (Virtual Machine Manager) → Windows-VM **snolla** с IIS + .NET Framework 4.8 CMS [[snolla-recovery-vm]] + - Container Manager → docker-стек: MSSQL Server 2019, MinIO 2020-07-13, Elasticsearch 7.10.1, imgproxy+nginx, traefik 2.6.6, gitea, и др. + - 11 клиентских сайтов под одной VM (snolla.com + 10 client TLDs: rimiz.ru, labtools.pro/ru, pilorama98.ru, tandemmebel.ru, emspb.ru, kupimknigi.spb.ru, maljarka.tandemmebel.ru, sestech.ru, ics-artmaterials.com) +- **Backup target NAS (живой):** [[kreknin-synology]] на 195.19.90.188 / kreknin.site, держит Hyper Backup репо. +- **Бэкап-задача:** "rsync Server 1", последняя успешная — 2026-05-09 05:06 (за 9 дней до краха). + +См. также: [[dead-synology-diskstation]], [[wd40efax-smr-cascade]]. + +## Хронология + +**2026-05-18 ~17:00 MSK:** инцидент — второй из 3 дисков RAID 5 на mёртвой синке вышел из строя. Пул past redundancy. Diagnose см. [[wd40efax-smr-cascade]]. + +**~17:30:** оценка вариантов. Выбран маршрут "восстановление на локальной Windows-машине пользователя" ([[windows-recovery-host]]) с гибридной архитектурой: VM для CMS (из OVA-экспорта) + docker-контейнеры для backend-сервисов. + +**~18:00:** SSH-доступ к [[kreknin-synology]] (изначально по паролю, потом через ssh-ключ). Разбор структуры `.hbk` репо. См. [[hyper-backup-structure-and-recovery]]. + +**~19:00:** старт Hyper Backup restore через DSM UI на удалённой синке во временную папку `/volume1/restore-tmp/`. Restore инициировал также создание новых шар `backup/`, `docker/`, `work/` на root уровне `/volume1/`. Длительность ~3 часа на 277 GB selective набор. + +**~21:00 — параллельно:** +- SFTP-pull `snolla.ova` (42.5 GB) на Windows — через FileZilla (SFTP сервис DSM требовалось включить отдельно, ACL/chroot нюансы). +- Локальная подготовка: установлены IIS, VirtualBox 7.2.8. +- На Windows запущен MSSQL контейнер 2019-latest пустым (для отладки compose). +- v2rayN на Windows: настроены routing rules "Bypass LAN" (`geoip:private` → direct), иначе VPN ловил inbound 80/443 traffic. + +**~22:00:** найден `MoreThenCms202605090301.zip` — ежедневный `.sql` дамп БД (101 MB compressed, 547 MB unpacked). Попытка restore через `sqlcmd -i` — упала на строке 457k из-за `$(function(){...})` в данных (jQuery JS в email-template таблицах). Sqlcmd интерпретировал `$()` как переменную. См. [[mssql-restore-pitfalls]]. + +**~23:00:** повторный sqlcmd с флагом `-x` (disable var substitution). Параллельно pull `/docker/personal/mssql/` (25 GB) как альтернатива — через ACL-fix `chmod -R a+rX`, потом `scp -O` (legacy SCP, обход chrooted SFTP). + +**2026-05-19 ночь (Claude автономно):** +- OVA import в VirtualBox завершился (42.7 мин) +- sqlcmd v2 длился ~2.5 часа, тоже падал. +- Решение: **bind-mount проблемы → named volume + `chown -R 10001:0`** через temp alpine container. Это сработало. См. [[mssql-container-data-restore]]. +- Имя dead synology в БД-файлах было всё с одинаковым паролем `fXkH4@8O%3pc` (production SA password из старого `docker-compose.yml`). Аккаунт `sa` оказался **disabled**, потребовался `mssql-conf set-sa-password` под `--user 0:0` (root) → re-enable. + +**~02-08:00 утра:** VM на VBox не загружалась. Кросс-гипервизорный crash. См. [[vbox-windows-stability-tuning]] — путь к стабильности через SCSI→SATA, отключение Hyper-V driver в Safe Mode, `--paravirtprovider kvm`, OS type Windows10_64, Guest Additions. + +**~09:30:** VM стабилизирована. Network drama #1: bridged через WiFi нестабильно (promiscuous mode проблемы у WiFi-адаптера). Switched VM nic1 на NAT + port forwarding в VBoxManage: host:23389→VM:3389, host:8022→VM:22, host:18080→VM:80, host:18180→VM:8080, host:18181→VM:8081, host:18189→VM:8089. + +**~10:00:** OpenSSH server установлен внутри VM. SSH-ключ для Claude в `C:\ProgramData\ssh\administrators_authorized_keys` (особая локация для admin-users, локализованная группа `Администраторы` через icacls). Default shell sshd переключен на PowerShell. + +**~10:30:** Web.config-патч во всех 4 сайтах VM: `Data Source=192.168.1.10` → `Data Source=10.0.2.2` (VBox NAT gateway = host). Encoding ловушка: `Set-Content` без `-Encoding utf8` записал UTF-16 LE — IIS вернул 500.19 invalid XML. Fix: `[System.IO.File]::WriteAllText` с `UTF8Encoding($true)` (BOM). См. [[cms-config-rewrite-pattern]]. + +**~11:00:** Traefik 2.6.6 запущен на хосте. Полный цикл правок: `--configFile=/traefik.yml` явно (не находил автоматом), named volume для `letsencrypt/` (bind-mount показывал `0777` Linux-side, traefik требует `0600`), 13 custom yml файлов пропатчены `192.168.1.15` → `host.docker.internal:18080`. docker.sock провайдер не работал (Docker Desktop особенности) → minio/imgproxy/elasticsearch traefik-labels переписаны как file-provider в `data/custom/`. См. [[traefik-on-windows-docker-desktop]]. + +**~11:30:** OpenWRT [[openwrt-router]] на 192.168.1.1: DHCP-резервация Windows-PC на 192.168.1.143 (его MAC 88:66:5A:2F:AA:68), port forwards 80→8000 и 443→4443 (host:8000/4443 ↔ traefik). VM получила старый MAC `02:11:32:2A:7C:B9` из DHCP-резервации `snolla` — IP 192.168.1.15 сохранился для совместимости с `snolla.yml` (но потом перешли на NAT, IP стал внутренним). + +**~12:00:** Public test через домен/чужой WiFi: **`https://snolla.com`, `https://pilorama98.ru`, `https://labtools.ru`, `https://labtools.pro`, `https://tandemmebel.ru`, `https://emspb.ru`, `https://kupimknigi.spb.ru`, `https://maljarka.tandemmebel.ru` отвечают `200 OK` end-to-end.** Recovery functionally complete. + +## После полного recovery — резервный pull + +В фоне на Windows: tar+ssh stream `C:\inetpub\wwwroot\` (8.9 GB) и `C:\stayer\` (2.27 GB) из VM в `C:\nas-recovery\vm-sites\` — как фолбэк если VM снова станет нестабильной (был один случай glitch network — лечился `ipconfig /release /renew` через `VBoxManage guestcontrol`). + +## Открытые вопросы / нюансы + +- **X-Forwarded-Proto/Host headers** между traefik и CMS не настроены → CMS делает redirect с `:4443` в URL. +- **MinIO / Azure storage** в CMS: connection string использует Azure SDK (AccountName=snolla, AccountKey=...), но в production реально работало с MinIO. Точная схема "не так, как казалось" по словам пользователя — ждёт пояснения. +- **acme.json renewal через HTTP-01** фейлится для доменов с DNS не на нашем IP. Решение — DNS-01 через REGRU (creds в `traefik/docker-compose.yml` env уже, в `traefik.yml` закомментировано). +- **VM long-term stability**: один случай network glitch уже был. Возможна планка scheduled task внутри VM — auto release/renew при детекции downtime. + +## Архитектурное замечание + +Сохранение работающей конфигурации не равно отказоустойчивости. Текущая инфраструктура [[recovery-architecture-snapshot]] сильнее, чем была (Hyper Backup проверен, восстановление практикой), но **single-point-of-failure всё ещё есть**: один Windows-PC, одна VM, один публичный IP, один роутер. Глобальная задача [[future-resilient-architecture-goals]] — на потом. diff --git a/.wiki/sources/vds-kzntsv-bootstrap-2026-05-20.md b/.wiki/sources/vds-kzntsv-bootstrap-2026-05-20.md new file mode 100644 index 0000000..7e02eb8 --- /dev/null +++ b/.wiki/sources/vds-kzntsv-bootstrap-2026-05-20.md @@ -0,0 +1,121 @@ +--- +title: VDS kzntsv bootstrap session 2026-05-20 +type: source +tags: [vds, bootstrap, rusonyx, traefik, portainer, gitea, verdaccio, registry, migration, kreknin] +ingested: 2026-05-20 +raw_path: ../../.tasks/vds-kzntsv-bootstrap.md +updated: 2026-05-20 +--- + +# VDS bootstrap session — 2026-05-20 + +Одна сессия, ~6 часов: активация Rusonyx VDS → 3-фазная подготовка инфра-стека → миграция трёх сервисов (gitea, verdaccio, registry) с восстановленного backup'а на [`kreknin-synology`](../entities/kreknin-synology.md). Решает первый пункт из [`future-resilient-architecture-goals`](../concepts/future-resilient-architecture-goals.md) — вынос инфра-сервисов с single-point-of-failure хоста. Production CMS остаётся на [`windows-recovery-host`](../entities/windows-recovery-host.md). + +Источник истины — `.tasks/vds-kzntsv-bootstrap.md`. Текущий live-state VDS — [`vds-kzntsv`](../entities/vds-kzntsv.md). + +## Контекст до сессии + +После аварии 2026-05-18 ([`dead-synology-diskstation`](../entities/dead-synology-diskstation.md) развалилась), CMS поднята на [`windows-recovery-host`](../entities/windows-recovery-host.md). Но инфра-сервисы (gitea, verdaccio, docker-registry, owncloud, hermes), которые тоже жили на мёртвой синке, остались только как restored backup на [`kreknin-synology`](../entities/kreknin-synology.md). User не хочет держать их на windows-recovery-host (тот уже перегружен CMS-стеком + это рабочая машина). + +Решение — отдельный облачный VDS у Rusonyx. Заказан 2026-05-19. Активирован 2026-05-20 — IP `89.253.255.94`, тариф 160 NVMe (6 vCPU / 8 GB / 160 GB / Ubuntu 24.04). + +## Хронология + +### Фаза 0 — pre-flight (DNS + кreds) + +User проставил DNS records в REGRU **до** активации сервера: `vds.kzntsv.site`, `*.vds.kzntsv.site`, `git.kzntsv.site`, `verdaccio.kzntsv.site`, `registry.kzntsv.site` → 89.253.255.94. DNS-cut решение: сервисы на kreknin остаются как есть, реальный switch произойдёт после standup'а на VDS (DNS уже там). + +Initial креды от Rusonyx: root + одноразовый пароль. Сохранены в `~/projects/.common/secrets/vds-kzntsv.env`. + +### Rusonyx onboarding pain + +VNC консоль изначально не открывалась. Помогла кнопка «Остановить VNC» в Управление сервером → Консоль (force-disconnect stale attachment). После этого VNC заработал. + +`apt update && apt upgrade && apt --fix-broken install && apt upgrade` пришлось гонять через VNC (Rusonyx сами рекомендуют: их virt бьёт SSH session, openssh-server restart рвёт connection). По дороге — 5+ dpkg interactive prompts: sshd_config conffile (chose 2 = keep local), cloud.cfg (chose N = keep), grub-pc target disk (1 = /dev/vda whole-disk MBR). Reboot после. + +Все эти подробности — в [`rusonyx-vps-onboarding-quirks`](../concepts/rusonyx-vps-onboarding-quirks.md). + +### Phase 1 — bootstrap (через SSH с root + pubkey push) + +1. Pubkey push через **plink** (PuTTY) с inline `-pw` (OpenSSH for Windows не поддерживает password в флаге). После push — ssh-key-only login. +2. Sudo user `vitya:Pryakhin10~` + group sudo + NOPASSWD грант (для автоматизации). +3. sshd harden через **drop-in `/etc/ssh/sshd_config.d/00-hardening.conf`** (`00-` prefix чтобы выиграть first-match precedence над `50-cloud-init.conf` который форсит `PasswordAuthentication yes`). `PermitRootLogin no` + `PasswordAuthentication no`. +4. ufw default-deny + allow 22/80/443 + DB-порты (5432/3306/27017/6379). +5. fail2ban (default sshd jail). +6. Docker CE 29.5.1 official APT repo + buildx + compose-plugin. vitya в группу docker. +7. Hostname `vds-kzntsv`, timezone Europe/Moscow. +8. Docker networks `proxy` + `shared-dbs` (external). + +### Phase 1 (cont) — Traefik v2.11 LTS + Portainer 2.21.5 + +- Traefik static config: 6 entrypoints (web/websecure/postgres/mariadb/mongo/redis), HTTP-01 LE challenge (DNS уже указан, проходит за 5 sec), dashboard на `traefik.vds.kzntsv.site` + basicAuth middleware из dynamic file provider. +- LE issued cert at first request (5 validators с разных AWS regions → 200 OK). + +**Portainer admin init — 4 итерации.** Hard min 12-char policy в Portainer 2.20+ (regression от 2.20.0+), CLI flag `--admin-password` + bcrypt **не bypass'ит** policy и фактически выходит broken (bcrypt сохраняется, но login fails). После проб с `$2y$`/`$2a$` prefix, single-quote escape, YAML list form, docker run direct — оказалось CLI flag тупо не работает в 2.21.5. Финал: голый `docker run`, без `--admin-password`, потом API admin/init с длинным паролем `Pryakhin9-VDS-2026` (18 chars). Деталь — [`portainer-2.21-admin-password-regression`](../concepts/portainer-2.21-admin-password-regression.md). + +API key сгенерирован через `/api/users//tokens`, local docker endpoint создан через `POST /api/endpoints` form-data. Сохранён в `vds-kzntsv.env`. + +### Phase 2 — Shared DB park с TLS через traefik + +User explicitly: «Я хочу доступ снаружи к базам! Через трафик» — поэтому DBs должны быть accessible from public internet с TLS. + +**Решение архитектуры — это main lesson:** +- Initial attempt: traefik TCP routers с `HostSNI('.vds.kzntsv.site')` + `tls.passthrough=true` → **работает для Mongo/Redis** (TLS-from-start), **не работает для Postgres/MariaDB** (STARTTLS-protocols, нет SNI в первых байтах). +- Switch на `HostSNI(*)` без `tls.*` (raw TCP forward) — работает для всех 4 DBs, traefik просто маршрутизирует по entrypoint port'у. DBs терминируют TLS сами. + +Подробно в [`traefik-tcp-passthrough-vs-starttls`](../concepts/traefik-tcp-passthrough-vs-starttls.md) и [`db-tls-self-signed-via-traefik-raw-tcp`](../concepts/db-tls-self-signed-via-traefik-raw-tcp.md). + +Self-signed certs у каждой DB (CN matches `.vds.kzntsv.site`), strong random hex32 passwords. Postgres alpine использует uid 70 — пришлось перейти на не-alpine `postgres:16` (uid 999, matches наш chown). Mongo 7 требует `--tlsCAFile` (chain of trust enforcement) — добавили self-signed как CA. Все 4 DBs верифицированы через openssl s_client + real protocol probe (pg_dumpall connect, mongosh ping, redis-cli PING, mariadb SELECT VERSION()). + +### Phase 3.1 — Gitea (миграция от Hyper Backup restored data) + +Источник = restored backup на kreknin, **не запущенный контейнер**. Это была ошибочная гипотеза в начале сессии — потеряли время и обиду user'а («ты почему ни хера не читаешь вики?»). Урок зафиксирован в feedback memory `feedback_read_wiki_first.md`. + +Pipeline: +1. tar+ssh stream `/volume1/docker/gitea/{data,postgres,docker-compose.yml}` с kreknin → vds (8m19s, ~2.7G total, ~5.4 Mbps). Sudo на kreknin требовал password (`Pryakhin9`) — passed через `echo Pryakhin9 | sudo -S tar c ...` inside ssh quotes. +2. На VDS — `docker run -d --rm postgres:9.6` mounted on restored datadir (postgres uid 999, datadir chown'd, chmod 700). Recovery после non-clean shutdown (last May 9 — последний Hyper Backup snapshot). +3. `pg_dump -U gitea gitea` → /tmp/gitea.sql (23 MB, 22400 lines). +4. Shared postgres 16: `CREATE ROLE gitea LOGIN PASSWORD 'gitea'; CREATE DATABASE gitea OWNER gitea ...;` через `docker exec postgres psql -U postgres -c "..."` (peer auth on Unix socket). +5. Restore `psql -U gitea -d gitea < gitea.sql`. Gitea автоматически мигрирует schema 9.6 → 16. +6. Stop temp pg9.6. +7. **Восстановленная data была вложена на уровень глубже** (`/data/data/git/...` вместо `/data/git/...`) — flatten layout. Gitea на первом старте перезаписал свежий app.ini (env-var driven), нужно было использовать оригинальный app.ini из вложенного `data/gitea/conf/app.ini` который содержал `INSTALL_LOCK=true`, `LFS_JWT_SECRET=...`, `SECRET_KEY=...` оригинала. +8. Patch app.ini под VDS: DOMAIN, SSH_DOMAIN, ROOT_URL → git.kzntsv.site; HOST → postgres:5432; SSH_PORT → 2222. +9. Gitea compose без `GITEA__database__*` env vars (пусть app.ini рулит). +10. Smoke: HTTP 200, `/api/v1/version` → `{"version":"1.25.5"}`, `/api/v1/repos/search` → 3 first repos (OpeItcLoc03/claude-skills и др.). Final: 132 repos, 4 users. + +### Phase 3.2 — Verdaccio (file rsync) + +1. rsync `storage/` + `config/` + `plugins/` от kreknin → /opt/stacks/verdaccio/ (~9G transferred, 17m36s, ~8.17 MB/s). Sudo на kreknin не нужен — vitya owns эти файлы. +2. Chown `10001:65533` (verdaccio uid). **Не делать `chown -R` на parent /opt/stacks/verdaccio** — съест permission на root dir, vitya не сможет писать compose. Только sub-dirs. +3. Compose с image `verdaccio/verdaccio:6` + traefik labels → `verdaccio.kzntsv.site`. +4. **Crashloop** — Node 22 в verdaccio:6 требует secret **точно** 32 chars в `/verdaccio/storage/.verdaccio-db.json` (не `.verdaccio-db` как старая версия). Restored secret был 64 chars (старая verdaccio накопила). Fix: `openssl rand -hex 16` = 32 chars, overwrite secret в JSON. +5. User reported: UI пусто. Причина — kreknin'овский config.yaml имеет `access: $authenticated` для всех packages, и анонимный посетитель не видит ничего. Нужно логиниться (`vitya` user в restored htpasswd). User'у объяснил, он залогинился — packages появились. + +### Phase 3.3 — Registry (GC + fresh install) + +1. Registry GC on kreknin локально (mount restored data, run `registry:2.8.3 garbage-collect`). **Mount должен быть PARENT dir, не sub-dir** — registry ожидает `/var/lib/registry/docker/registry/v2/...`, а не `/var/lib/registry/registry/v2/...`. С правильным mount + `-m` (modify=delete) flag — 99G → **35G** (64G freed). +2. Запущен rsync 35G с kreknin → /opt/stacks/registry/. ETA ~50 min при 2 MB/s. +3. **Mid-flight user reconsiders**: «может зря тащим старые образы? Могу новых наделать». Decision: kill rsync, fresh install. User accepts loss of old images. +4. Fresh registry с htpasswd (vitya/Pryakhin9), `REGISTRY_STORAGE_DELETE_ENABLED=true`, CORS headers для UI. +5. Joxit Registry UI (joxit/docker-registry-ui) на `registry-ui.vds.kzntsv.site` с `DELETE_IMAGES=true` для manual cleanup через GUI. + +Подробно про GC: [`registry-gc-mount-and-modify-flag`](../concepts/registry-gc-mount-and-modify-flag.md). + +## Decisions log + +- **Tariff 160 NVMe Rusonyx** (a не 80 SSD + addon) — operational simplicity overweights ₽1000/мес savings (decision 2026-05-19). +- **Ubuntu 24.04 LTS** vs Debian 12 — LTS support до 2029, docker official APT primary target Ubuntu (decision 2026-05-19). +- **Traefik v2.11 LTS** vs v3 — user explicit choice (совместимость с windows-recovery-host где v2.6.6). +- **DB access from outside через traefik** — user explicit reversal от docker-network-only Q1 answer. +- **DB TLS = self-signed** для starts, LE-cert sidecar deferred — operational simplicity. +- **MongoDB include immediately** — user explicit. +- **Hermes defer** — user explicit. +- **Registry — fresh install, no migration** — user mid-flight reversal (могу новых наделать). +- **`HostSNI(*)` + no tls.* для всех DBs** — uniform config работает для всех protocols (STARTTLS + TLS-from-start). +- **Portainer admin pass `Pryakhin9-VDS-2026` (18 chars)** вместо запрошенного `Pryakhin9` (9 chars) — Portainer 2.21+ hard min 12-char policy, `--admin-password` CLI flag broken в 2.20+. Сохранено в `vds-kzntsv.env`. + +## Архитектурное замечание + +VDS kzntsv = первый шаг по closing SPOF gap из [`future-resilient-architecture-goals`](../concepts/future-resilient-architecture-goals.md). Раньше всё крутилось на одной физической коробке ([`dead-synology-diskstation`](../entities/dead-synology-diskstation.md) — упала; [`windows-recovery-host`](../entities/windows-recovery-host.md) — тоже SPOF). Теперь инфра-сервисы (git/npm/registry/dbs) живут на отдельном cloud-host'е. Это **не** полное multi-host resilience: VDS сам по себе тоже single host. Но decouples infra и production CMS, что критично. + +Backup pipeline VDS → kreknin (см. follow-up task [`vds-backup-rsync-kreknin`](../../.tasks/vds-backup-rsync-kreknin.md)) — следующий шаг.